AngularJS の $http サービスのXSRF対策について調べた
AngularJS の $http サービスのXSRF対策について調べた
AngularJS の $http サービスにはXSRF(CSRF)の対策の機能が実装されています。
(AngularJSでは、cross-site request forgeries
の略を XSRF
と呼んでいる場面が多くみられるので、ここではXSRFで統一します。)
AngularJS側の動作
XHRが実行された際に $http
サービスは、Cookieに XSRF-TOKEN
があれば、 X-XSRF-TOKEN
という名前でHTTPヘッダーにセットします。※ この名前は変更することができます。
サーバ側の対応
送られてきたCookieに含まれる XSRF-TOKEN
の値と HTTPヘッダーのX-XSRF-TOKEN
の値が合っていることを確認します。
自分のドメイン上で実行されるJavaScript以外はCookieを読み込むことが出来ないため、 サーバーはXHRが自分のドメイン上で実行されて送られてきた事を保証することが出来ます。
実装して確認してみる
簡単な実装を行い動作を確認してみます。 テキストボックスに入力した値をXHRでサーバにリクエストし、Tokenの検証が成功したら入力した値を返し、テキストボックスの直下に値を表示させます。 構成としては、sinatraで画面(html+AngularJS)をレンダリングし、そのレンダリングした画面でXHRが実行するようにします。
XHRでメッセージをリクエストし、レスポンスで同じメッセージが返ってくれば成功です。
require 'sinatra' require 'sinatra/reloader' require 'sinatra/cookies' require 'json' require 'active_support' require 'active_support/core_ext' # 画面をレンダリングする get '/' do # Tokenを生成 token = SecureRandom.base64(32) response.set_cookie( 'XSRF-TOKEN', value: token, # jsからcookieを読み取る必要があるので httponly を off にする httponly: false, secure: false ) erb :index end # Tokenの検証に成功したら、入力した文字をそのまま返す post '/add_message' do if same_xsrf_token?(cookies['XSRF-TOKEN']) { message: request_params['message'] }.to_json else status 403 { error: 'CSRF検証に失敗しました' }.to_json end end private def request_params request_body = request.body.read return {} if request_body.blank? JSON.parse(request_body) end def same_xsrf_token?(token) return false if token.blank? http_headers = request.env.select { |k, v| k.start_with?('HTTP_') } token == http_headers['HTTP_X_XSRF_TOKEN'] end
フロント側
<!DOCTYPE html> <html ng-app='App'> <head> <meta charset="utf-8"> <title>angularjs xsrf</title> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script> <script src="js/controller.js"></script> </head> <body ng-controller="AppController"> <input type="text" ng-model="message"> <input type="button" value="送信" ng-click="send_message()"> <h1>{{receive_message}}</h1> </body>
var app = angular.module('App', []); app.controller('AppController', ['$scope', '$http', function($scope, $http){ $scope.send_message = function(){ $http({ method: 'POST', url: 'add_message', data: { message: $scope.message } }) .success(function(data, status, headers, config){ $scope.receive_message = data.message; }) .error(function(data, status, headers, config){ $scope.receive_message = data.error; }); } }]);
動作させてみる
画面で確認
- 無事リクエストしたメッセージが取得できていることを確認しました。
- ちなみに検証に失敗するとこうなります。
リクエストの内容を確認する
POST /add_message HTTP/1.1 Host: localhost:4567 Connection: keep-alive Content-Length: 20 Accept: application/json, text/plain, */* Origin: http://localhost:4567 X-XSRF-TOKEN: VGqbpCECjRRXF6XJFwD4IJA0zxzxdobSr4mnMJieyvI= User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36 Content-Type: application/json;charset=UTF-8 Referer: http://localhost:4567/ Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Cookie: XSRF-TOKEN=VGqbpCECjRRXF6XJFwD4IJA0zxzxdobSr4mnMJieyvI=
Cookie XSRF-TOKEN
に保存されているのと同じ値が、 X-XSRF-TOKEN
が付与されていることを確認
JWT(+JWS)について調べた (JWTの検証)
前回、こちらでHMAC SHA256で署名したJWT(+JWS)を作成していきましたが、今回はその検証を行っていきます。
rfc7519
に検証手順があるので、それを参考にしつつ流れを確認しました。
- JWTをheader、payload、signatureに分割
- 分割したheader、payload、signatureをBASE64urlでデコード
- headerから署名アルゴリズムを確認する
- 確認した署名方法でJWTの検証を行なう
これに加えて有効期限の検証も行っていきます。
とにかくコードを書いていきます。 今回はコードの中で説明をしていく形になりますが、順番的にはこんな感じで検証していくのが良いかと思われます。
# -*- coding: utf-8 -*- require 'base64' require 'json' require 'openssl' jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE' # JWTをheader、payload、signatureに分割 encoded_header, encoded_payload, encoded_signature = jwt.split('.') # 分割したheader、payload、signatureをBASE64urlでデコード decoded_header = Base64.urlsafe_decode64(encoded_header) # パディングがなくてもよしなにデコードしてくれる decoded_payload = Base64.urlsafe_decode64(encoded_payload) decoded_signature = Base64.urlsafe_decode64(encoded_signature) # headerから署名アルゴリズムを確認する # 今回は形式はJWTでHS256以外のものは対応しないので、それ以外は処理しないようにします header = JSON.parse(decoded_header) payload = JSON.parse(decoded_payload) unless header['typ'] == 'JWT' && header['alg'] == 'HS256' puts 'error: 対象外の形式です。' return end # 確認した署名方法でJWTの検証を行なう secret = 'secret' verification_signature = OpenSSL::HMAC.digest( OpenSSL::Digest::SHA256.new, secret, "#{encoded_header}.#{encoded_payload}" ) if decoded_signature != verification_signature puts 'error: 検証が失敗しました。' return end # 有効期限を設けていれば、その検証も行なう unless Time.now.to_i < payload['exp'] puts 'error: 有効期限が過ぎています。' return end puts '検証OK'
参考
JWT(+JWS)について調べた
JWT とは
JSONオブジェクトで、安全に2者間で情報をやりとりするための表現方法です。 RFC7519で定義されています。
JWS とは
JSON Web Signatureの略、JWTのエンコード形式、デジタル署名、もしくはMACを行ったメッセージの表現 RFC7515で定義されています。 他には、暗号化するための仕様としてJSON Web Encryption (JWE)なんかがあります。
今回は、使われているケースが多いJWT+JWSの組み合わせのパターンでの実装を調べていきたいと思います。
特徴
- コンパクトである
- サイズが小さいので、URL、POSTパラメータ、またはHTTPヘッダー内で送信できます。
- 自己完結されている
- url-safeである
- URLで使用できる文字列のみで構成されています。
どんなときに使うのか
認証
- こちらが一般的な使われ方のようです。
- ユーザーがサービスにログインしJWTを発行してらもらい、その発行されたJWTで他に許可された異なるドメインのリソースを取得する。などの使い方ができます。
情報の交換
OpenID ConnectやOAuthと組み合わせて使うケースもあるようです。
JWTの構造
- ヘッダ
- ペイロード
- 署名
これらをBase64エンコーディング(urlsafe、パディング無し)に変換し、.
区切りで結合します。
こんな形になります。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE
ヘッダ
ヘッダには、このトークンのタイプ、使用されているハッシュアルゴリズムなどの情報で構成されます。
この例では、
ハッシュアルゴリズムが、HS256
(HMAC SHA-256アルゴリズム)
トークンのタイプが、JWT
ということを示しています。
{ "alg": "HS256", "typ": "JWT" }
ペイロード
ペイロードは以下のような形式になります。 ペイロードにはクレームを含んでいます。 (クレームとは、エンティティ(通常はユーザー)と追加のメタデータのことを指します。)
クレームには、以下3種類があります。
予約済み
公開済み
個人用
- これらは、それらの使用に同意する当事者間で情報を共有するために作成されたカスタムクレームです。
{ "sub": "1234567890", "name": "John Doe", "admin": true, "exp": 1540652400 }
有効期限(exp)は、 NumericDate
である必要があるので、ここでは、UNIXTIMEを入れています。
https://tools.ietf.org/html/rfc7519#section-4.1.4
署名
先ほど、紹介したJOSE ヘッダ、JWSペイロード、(Base64urlエンコード済)を、
HMAC SHA-256
アルゴリズムで署名し,
[JWS]に指定された形で署名をbase64urlエンコードすると, このエンコード済JWS署名を作り出せます。
RubyでJWTを作ってみる
# -*- coding: utf-8 -*- require 'base64' require 'json' require 'openssl' header = { alg: 'HS256', typ: 'JWT' } payload = { sub: '1234567890', name: 'John Doe', admin: true, exp: 1540652400 } secret = 'secret' encoded_header = Base64.urlsafe_encode64(header.to_json).delete('=') encoded_payload = Base64.urlsafe_encode64(payload.to_json).delete('=') signature = OpenSSL::HMAC.digest( OpenSSL::Digest::SHA256.new, secret, "#{encoded_header}.#{encoded_payload}" ) encoded_signature = Base64.urlsafe_encode64(signature).delete('=') jwt = [ encoded_header, encoded_payload, encoded_signature, ].join('.') puts '[jwt] => ' + jwt
結果
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE
値の確認
こちらのjwt.ioというサイトのDebuggerツールで、値の確認ができます。
JWTの検証
こちらは、別の記事でまとめていく予定です。
window.postMessage APIの使い方を調べた
postMessageとは
- HTML5で拡張された機能
- 任意のウィンドウへクロスオリジン通信が可能
- セキュアである
- 双方向の通信チャンネルが確立されてから通信を行う
- 悪意あるWebサイトから知らないうちにされないようにしているため
使い方(送信)
win.postMessage("メッセージ", "http://www.example.com");
メッセージを受け取りたい対象に対してpostMessage
を呼びます。
コードで示している win
は以下を指します。
- 自分のドキュメントウィンドウ内に生成されたiframe
- JavaScriptで開いたポップアップウィンドウ
- 自分のドキュメントウィンドウを含むウィンドウ
- 自分のドキュメントを開いたウィンドウ
第一引数に渡したいメッセージ(文字列)、第二引数に宛先のオリジンを指定します。
受け取り方(受信)
addEventListener
を使って、message
イベントを登録します。
addEventListener('message', function(event) { // 受け取ってからの処理 }, false);
この event
には、以下3つのプロパティが用意されています。
- event.data
- 実際のメッセージ内容(文字列)
- event.origin
- メッセージを送ってきたページの生成元
- event.source
- メッセージを送ってきた window オブジェクトの参照
セキュリティ上の注意点
postMessageの第二引数にワイルドカードを指定しない
postMessageの第二引数に '*' を指定すると、どのオリジンに対してもメッセージを送信することができるため、意図して使わない限り指定しないようにしたほうが良さそうです。
受け取ったメッセージの送信元を確認する
addEventListener
は送られてきたすべてのメッセージを受け取ることができるため、信頼できないページからのメッセージも受け取ることができます。
そのため、受け取ったら必ず以下のように信頼できる送信元であるかチェックをしたほうが良いです。
addEventListener('message', function(event) { if (event.origin !== 'http://www.example.com/') { return; } }, false);
補足
最近は文字列でなくても良い
最新のブラウザではpostMessageの第一引数に、様々なデータオブジェクトを受け付けることができるようになっています。 ただ、IE8,IE9の場合はこの方法は使えないので、互換性を持たせたいのであればJSONを使うほうが良さそうです。 (もう、サポート切れているし意識しなくても良いかな。)
IE8以下は、addEventListener
は使えないので attachEvent
を使う
詳しくはこちら https://qiita.com/39_isao/items/506e08031adfca9a5933
参考
CORSの動作を確認した
前回、こちらのエントリーにて、XMLHttpRequestが SOP の対象となった場合の動作を確認しました。
今回は、Cross-Origin Resource Sharing (CORS)の仕様したがって、SOPで制限されている クロスオリジンへの XMLHttpRequest(XHR) を可能にします。
CORSについては以下を参照すると良いでしょう。
リクエストは2パターン
リクエストは2パターンあります。条件によってそれぞれ使い分けされます。
それぞれ動作を確認していきます。
シンプルなリクエスト
シンプルなリクエストとは
シンプルなリクエストとは、以下のすべての条件に合うものです。
※HTTP アクセス制御 (CORS)から抜粋
許可されたメソッドは以下に限る:
- GET
- HEAD
- POST
ユーザエージェントが自動的に設定するヘッダ (Connection や User-Agent など) を除き、手動で設定できるヘッダは以下に限る:
- Accept
- Accept-Language
- Content-Language
- Content-Type
Content-Type ヘッダで許可される値は以下に限る:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
実際にやってみる
前回の記事で、「異なるオリジンにXHRをしてみる」というセクションで、異なるオリジンにXHRをしSOP の制御対象となることを確認しましたが、このリクエストを許可するようにします。
前回と同じ環境で、異なるオリジンのXHRが許可されるような条件を加え試します。
前回の環境
- XHR送信側サーバ(Sinatra):
http://test.example.com:4567/
(HTML,JSをレンダリングし、そこからXHRを送信します。) - XHR受信側サーバ(Sinatra):
http://test.example.com:4568/
- リクエストメソッド: POST
- レスポンス:
Failed to load http://test.example.com:4568/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://test.example.com:4567' is therefore not allowed access.
というエラーがブラウザに表示され、Response が受け取れない
Accept:*/* Accept-Encoding:gzip, deflate, br Accept-Language:ja Connection:keep-alive Content-Length:0 Content-Type:text/plain Host:localhost:4567 Origin:http://test.example.com:4567 Referer:http://test.example.com:4567/tools User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36
加える条件
XHR受信側サーバ(http://test.example.com:4568/
) の Response Header に以下を返すようにします。
Access-Control-Allow-Origin: http://test.example.com:4567/
結果
レスポンスを受け取ることができました。
レスポンスヘッダ
Access-Control-Allow-Origin:http://test.example.com:4567 Connection:keep-alive Content-Length:116 Content-Type:text/html;charset=utf-8 Server:thin X-Content-Type-Options:nosniff X-Frame-Options:SAMEORIGIN X-XSS-Protection:1; mode=block
レスポンスボディ(レスポンスメソッド とUserAgentを返すようにしています。)
POST from [Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36]
Access-Control-Allow-Origin ヘッダ
上記の動きで確認した通り、Access-Control-Allow-Origin
は、指定した Origin のアクセスを許可することができます。
リクエストで送られたOrigin
ヘッダの内容とこの Access-Control-Allow-Origin
が同じであればアクセス許可されるという仕組みです。
(Origin
ヘッダは、XHRで異なるOrigin
へ送信するときに自動的に付与されます。)
どの Origin でもアクセスを許可させる場合は、Access-Control-Allow-Origin: *
という形にもできます。
ただし、これは、後ほど説明する クレデンシャルを含むリクエスト
には使うことができません。
プリフライトリクエスト
以下のようなリクエストのときに、プリフライトリクエストを行います。
GET、HEAD、POST 以外のメソッドを使用した場合。また application/x-www-form-urlencoded、multipart/form-data、または text/plain 以外の Content-Type とともに POST を使用してリクエストを行う場合、例えば application/xml または text/xml を使用して POST で XML のペイロードをサーバーへ送るときは、リクエストでプリフライトを行います。
実際にやってみる
ひとまず、ダメ元で、先ほどシンプルなリクエストで行ったもののリクエストメソッドをPOSTからPUTに変更しただけの状態で試してみます。
デベロッパーツールのConsoleには、以下のようなエラーが表示されました。
xhr.js:18 OPTIONS http://test.example.com:4568/ net::ERR_ABORTED send_request @ xhr.js:18 onclick @ tools:22 tools:1 Failed to load http://test.example.com:4568/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://test.example.com:4567' is therefore not allowed access. The response had HTTP status code 404. xhr.js:18 XHR failed loading: OPTIONS "http://test.example.com:4568/".
OPTIONSメソッドでアクセスしようとしたが、サーバ側でOPTIONSを受け取る処理を行っていなかったので、404で返ってきているみたいですが、 なぜ、PUTではなく、OPTIONSで送信されたのでしょう。
プリフライトリクエストの流れ
OPTIONSで送信されたのには、以下の理由があります。
HTTP アクセス制御 (CORS)には、こう記述されています。
シンプルなリクエスト (前述) とは異なり、"プリフライト" リクエストは始めに、実際のリクエストを送信しても安全かを確かめるために他ドメインのリソースへ向けて OPTIONS メソッドを使用して HTTP リクエストを送信します。クロスサイトリクエストはユーザーデータに影響を与える可能性があるため、このようにプリフライトを行います。
つまり、流れとしては、
- [クライアント]OPTIONSメソッドを送信
- [サーバ]許可しているメソッドやカスタムヘッダをResponse Headerに含め返します。
- メソッドを許可: Access-Control-Request-Method
カスタムヘッダを許可: Access-Control-Request-Headers (こちらは後ほど詳しく説明します。)
[クライアント]元々送ろうとしていたリクエストを送信する
・・・あとは、通常通り
改めて実際にやってみる
今回は、PUTメソッドを許可するので、サーバ側に以下の設定を加えます。
- OPTIONSメソッドを受け取るようにする
- OPTIONSメソッドを受け取ったら、以下のヘッダ・値を返すようにする
Access-Control-Allow-Origin: http://test.example.com:4567
Access-Control-Allow-Methods: PUT
そして送信!
デベロッパーツールのConsoleには、以下のように、OPTIONSとPUTを送信している様子が見えます。
xhr.js:18 XHR finished loading: OPTIONS "http://test.example.com:4568/". send_request @ xhr.js:18 onclick @ tools:22 XHR finished loading: PUT "http://test.example.com:4568/".
リクエストヘッダ(OPTIONS)
OPTIONS / HTTP/1.1 Host: localhost:4568 Connection: keep-alive Access-Control-Request-Method: PUT Origin: http://test.example.com:4567 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36 Accept: */* Referer: http://test.example.com:4567/tools Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.8,en;q=0.6
レスポンスヘッダ(OPTIONS)
HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 Access-Control-Allow-Origin: http://test.example.com:4567 Access-Control-Allow-Methods: PUT X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Connection: close Server: thin
リクエストヘッダ(PUT)
PUT / HTTP/1.1 Host: localhost:4568 Connection: keep-alive Content-Length: 0 Accept: */* Origin: http://test.example.com:4567 Accept-Language: ja User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36 Content-Type: text/plain Referer: http://test.example.com:4567/tools Accept-Encoding: gzip, deflate, br
レスポンスヘッダ(PUT)
HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 Access-Control-Allow-Origin: http://test.example.com:4567 Content-Length: 115 X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Connection: keep-alive Server: thin
うまくリクエストを送ることができました。
クレデンシャルを含むリクエスト
同一オリジンであれば、クレデンシャルを含むリクエストはデフォルトで許可されますが、 異なるオリジンへクレデンシャルを含むリクエストを送るときは、注意が必要です。
ちなみに、クレデンシャルというのは、Cookieの送信、BASIC認証を指します。
実際にやってみる
ひとまず、そのままではできないことを確認しましょう。
こんな感じで、XHRでCookieを含めて送信するようにします。
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
デベロッパーツールのConsoleには、以下のようなエラーが表示されました。
tools:1 Failed to load http://test.example.com:4568/: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. Origin 'http://test.example.com:4567' is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute. xhr.js:19 XHR failed loading: POST "http://test.example.com:4568/".
なるほど、Access-Control-Allow-Credentials
ヘッダを追加というものをつけて上げる必要があると
というわけで、サーバに以下のヘッダを返すようにします。
Access-Control-Allow-Credentials:true
注意点としては、上記を追加する場合は、Access-Control-Allow-Origin:*
の指定ができません。
必ず、オリジンを指定する必要があります。
改めて実際にやってみる
では、引き続きやってみましょう。
シンプルなリクエスト・プリフライトリクエストが必要なものどちらも行いましたが、今度は、成功しました。
デベロッパーツールのConsole
xhr.js:19 XHR finished loading: POST "http://test.example.com:4568/". send_request @ xhr.js:19 onclick @ tools:21 xhr.js:19 XHR finished loading: OPTIONS "http://test.example.com:4568/". send_request @ xhr.js:19 onclick @ tools:22 XHR finished loading: PUT "http://test.example.com:4568/".
CORSのヘッダについて
CORSのリクエストヘッダ/レスポンスヘッダの詳細については、HTTP アクセス制御 (CORS)に詳しく書いてあるので、ここを読めばバッチリでしょう。
参考
XMLHttpRequestが Same-Origin Policy の対象となった場合の動作を確認した
XMLHttpRequest(以降「XHR」と表記)が Same-Origin Policyの対象となった場合の動作を実際にリクエストを送信し、動作を確認しました。
今回はSame-Origin Policy周りの検証をしていくにあたっての第一弾として、今後はもう少し踏み込んだ内容でやっていく予定です。
Same-Origin Policy(SOP)とは
Same-Origin Policy(以降「SOP」と表記) は、日本語では、同一オリジンポリシー
、同一生成元ポリシー
などと呼ばれています。
あるオリジン(スキーム、ホスト、ポートの組み合わせ)から読み込まれた文書やスクリプトについて、そのリソースから他のオリジンのリソースにアクセスできないように制限するものです。
逆に、同じオリジンであれば、制限なしにアクセスが行えます。
今回は、XHRのSOPの動作を確認をしていきますが、他には、DOMアクセス、Cookieに関するSOP。プラグインなどでは、Java,Flash、Silverlightなどにもそれぞれ個別のSOPが存在します。
詳しくは、MDN web docsのこちらのページが参考になります。
動作を確認する
簡単な検証用webアプリケーションをSinatraで作成しました。
アプリケーションは以下のような機能をもっています。
- 画面から好きなURI宛にXHRをGet、Postで送信することができる。
- 画面からXHRの結果(Response BodyとStatus Code)を見ることができる。
- Response Bodyには、送った Request Method とUserAgentを表示します。
- ルートパスにGet、Postリクエストを送ることができ、リクエストの内容をコンソールに表示することができる。
画面はこんな感じです。バッと雑に作って確認していきます。
これを、先ほど説明したアプリケーションをそれぞれポートを変えて2つ起動し、XHRでやり取りできるか、できないかを確認します。
http://test.example.com:4567/
http://test.example.com:4568/
※ホスト名は検証したものとは異なります。
同一のオリジンにXHRをしてみる
まずは、アプリケーションの機能で、画面から自分宛にXHRを送信します。
http://test.example.com:4567/tool
にアクセスすると、
画面から好きなURI宛にXHRをGet、Postで送信することができるツールが開くので、
ここから、http://test.example.com:4567/
宛にXHRを送信します。
結果としては、もちろんですが、リクエストの送信・レスポンスの取得が成功しました。
XHR finished loading: GET "http://test.example.com:4567/". XHR finished loading: POST "http://test.exapmle.com:4567/".
- サーバ側にXHRが届いていることを確認
GET from [Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36] XXX.XXX.XXX.XXX - - [XX/Sep/2017:XX:XX:XX +0900] "GET / HTTP/1.1" 200 - 0.0038 POST from [Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36] "Content-Type [text/plain]" "Body [Request Body]" XXX.XXX.XXX.XXX - - [XX/Sep/2017:XX:XX:XX +0900] "POST / HTTP/1.1" 200 19 0.0005
- 検証用のアプリケーションの画面からも確認
異なるオリジンにXHRをしてみる
ポートを変えて同じアプリケーションを起動し、そこへXHRを送信してみます。
- デベロッパーツールのConsoleから確認
要求されたリソースに Access-Control-Allow-Origin
ヘッダーがないので、アクセスが許可されていない旨のメッセージがブラウザに表示されました。(Get、Post両方とも同じ内容)
Failed to load http://test.example.com:4568/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://test.example.com:4567' is therefore not allowed access.
- リクエスト自体は、サーバ側に届いているようです。
GET from [Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36] XXX.XXX.XXX.XXX - - [XX/Sep/2017:XX:XX:XX +0900] "GET / HTTP/1.1" 200 - 0.0005 POST from [Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36] "Content-Type [text/plain]" "Body [Request Body]" XXX.XXX.XXX.XXX - - [XX/Sep/2017:XX:XX:XX +0900] "POST / HTTP/1.1" 200 19 0.0005
- 検証用のアプリケーションの画面からも確認
Get、Postどちらも値が取得できませんでした。
まとめ
異なるオリジンへは、特に対策を行わない限りXHRでResponseの値を取得することができませんでした。 また、リクエストはサーバ側に届きますが、クライアント側でレスポンスは取得できないということがわかりました。
参考
SOPに関しては、以下を参考にしました。
TwitterAPIを使ってOAuth2.0のフロー(Client Credentials)を確認した。
先日、以下のエントリーにて、4つあるOAuth2.0の認可フロー(Grant Type)からAuthorization Codeの流れを確認しました。
今回は、TwitterAPIにOAuth2.0でツイートの検索ができるよう認可してもらい、実際に検索結果のjsonが返ってくるところまでやっていきたいと思います。
Client Credentialsの流れは動画で確認できます。
参考
OAuth2.0(Client Credentials)について
Twitterの認可・仕様などについて
やること・ゴール
- 前回同様、Rubyで簡易的な実装で流れを掴んで行きたいと思います。
- 上記にも書きましたが、TwitterAPIにOAuth2.0でツイートの検索ができるよう認可してもらい、実際に検索結果のjsonが返ってくるところまでやっていきたいと思います。
Client Credentialsについて
Client Credentialsは他の認証フロー3つとは、少し異なります。
- ユーザ
- リソースを保持するサービス・認可をするサービス
- 第三者(リソースを保持するサービスのリソースを使いたい)
上記3人の登場人物がいたとすると、Client Credentials以外の認証フロー(Authorization Code, Implicit, Resource owner password credentials)は、 第三者がリソースへのアクセスの認可をしてもらうためのものです。 それに対して、Client Credentialsは認可を必要としていないリソースへアクセスする場合に使用します。
具体的にどういうこと?というと、 例えば、今回実装するTwitterAPIでは、 ‘Application-only authentication’ という認可のタイプ、以下の操作をすることが許可されます。 (TwitterAPIでは、この認可タイプ以外は、OAuth2.0に対応していないようです。)
- ツイートの検索
- ユーザのツイートの取得
- ユーザー情報を取得
- 任意のアカウントの友人やフォロワーの取得
など
反対にアクセスできないものは、
- ツイートの投稿
- ダイレクトメッセージの送信
など
つまり、ユーザ認可が必要のない公開されている内容のみ使用したい場合に使います。
恐らくアクセス数の制御のためだと思われます。
大まかな流れ
Client Credentials の説明を上記で行った通り、今回TwitterAPIでは、 ‘Application-only authentication’ の認可をもらいます。
流れとしては以下です。公式ページの流れに沿って実装していきます。
Application-only authentication — Twitter Developers
- consumer_keyとconsumer_secretを取得する
- consumer_keyとconsumer_secretをBase64でエンコードし、bearer_tokenを取得する
- bearer_tokenを使って、特定のアカウントのツイート一覧を表示する
consumer_keyとconsumer_secretを取得する
Twitter Developersにログインし、 consumer_keyとconsumer_secretをもらいます。 手順は検索すると多くの記事がでてくるので割愛します。
consumer_keyとconsumer_secretをBase64でエンコードし、bearer_tokenを取得する
ここは公式ページの Step 1: Encode consumer key and secret
, Step 2: Obtain a bearer token
に該当します。
以下のコードにあるように、consumer_key, consumer_secretをURIエンコードし、 :
で結合後、Base64エンコーディングし、リクエストを送信します。
# -*- coding: utf-8 -*- require 'uri' require 'net/http' require 'openssl' require 'base64' consumer_key = URI.encode({ consumer_key }) consumer_secret = URI.encode({ consumer_secret }) credential = Base64.strict_encode64(consumer_key + ':' + consumer_secret) uri = URI.parse('https://api.twitter.com/oauth2/token') https = Net::HTTP.new(uri.host, uri.port) https.use_ssl = true request_body_params = 'grant_type=client_credentials' request_header = { 'Authorization': "Basic #{credential}", 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Content-Length': request_body_params.length.to_s, 'Accept-Encoding': 'gzip' } request = Net::HTTP::Post.new(uri.request_uri, request_header) request.body = request_body_params response = https.request(request) puts response.code puts response.body
成功するとステータスコード200で以下のレスポンスが返ってきます。
access_token
が bearer_token
です。
{ "token_type":"bearer", "access_token": { ここにアクセストークンが表示されます。 } }
bearer_tokenを使って、特定のアカウントのツイート一覧を表示する
ここは公式ページの Step 3: Authenticate API requests with the bearer token
に該当します。
ひとまず、公式のサンプル通りに実装していきます。
# -*- coding: utf-8 -*- require 'uri' require 'net/http' access_token = { access_token } uri = URI.parse('https://api.twitter.com/1.1/statuses/user_timeline.json') https = Net::HTTP.new(uri.host, uri.port) https.use_ssl = true request_header = { 'Authorization': "Bearer #{access_token}", 'Accept-Encoding': 'gzip' } request_params = { count: '100', screen_name: 'twitterapi' } uri.query = URI.encode_www_form(request_params) request = Net::HTTP::Get.new(uri.request_uri, request_header) response = https.request(request) puts response.code puts response.body
成功するとステータスコード200でそれっぽいレスポンスがjsonで返ってきます。
せっかくなので、自分のツイートの検索してみます。 パラメータの仕様はこちらで確認しました。
GET search/tweets — Twitter Developers
# -*- coding: utf-8 -*- require 'uri' require 'net/http' access_token = { access_token } uri = URI.parse('https://api.twitter.com/1.1/search/tweets.json') https = Net::HTTP.new(uri.host, uri.port) https.use_ssl = true request_header = { 'Authorization': "Bearer #{access_token}", 'Accept-Encoding': 'gzip' } request_params = { q: '腹痛に耐えながらnginxをビルドし直し自サービスをhttp2化した。', lang: 'ja', result_type: 'mixed', count: '1' } uri.query = URI.encode_www_form(request_params) request = Net::HTTP::Get.new(uri.request_uri, request_header) response = https.request(request) puts response.code puts response.body
結果のjsonはちゃんと見ていませんが screen_name
に自分のアカウントが表示されていたので良さそうな気がします。