OAuth 徹底入門の演習を解いてく第3章
だからあれほど並行してやるなというのに・・・なぜ私はこう並行して手を出してしまうのか。
Spring Security のドキュメントを読む方で疲弊してくると今度こちらをやっている。
OAuth徹底入門 セキュアな認可システムを適用するための原則と実践
- 作者:Justin Richer,Antonio Sanso
- 発売日: 2019/01/30
- メディア: 単行本(ソフトカバー)
JavaScript は初心者だが、まあほぼ写経なのでなんとかなるだろう。
事前準備
コードはこちらから
GitHub - oauthinaction/oauth-in-action-code: Source code for OAuth 2 in Action
2章までは基礎的な知識が書いてあり、OAuth ってそもそも何かよくわからない人とかは読んでおいた方が良さそう。なんとなくでも用語がわからないと今後の演習は厳しいと思う。
3 シンプルな OAuth クライアントの構築
3.1 認可サーバーへのOAuthクライアントの登録
$ npm --version 6.14.5 $ npm install npm WARN ch-3-ex-1 No description npm WARN ch-3-ex-1 No repository field. npm WARN ch-3-ex-1 No license field. audited 236 packages in 5.583s 1 package is looking for funding run `npm fund` for details found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details
色々警告が出ている。まあ、そもそもサンプルコードが相当昔に作られているので仕方がない気はする。
3.2 Authorization Code Grantによるトークン取得
- 一番面倒なシーケンス
- 一番基本となるシーケンス。これが理解できれば本筋は違わない(はず
3.2.1 認可リクエストの送信
authorizationServerのClientsの情報は直値でコード内に埋め込んである(当然だけど本当は良くない。ClientSecret 露出してるし)
$ node authorizationServer.js OAuth Authorization Server is listening at http://127.0.0.1:9001
clietn.js
の app.get(...)
を実装する
app.get('/authorize', function(req, res){ var authorizeUrl = buildUrl(authServer.authorizationEndpoint, { response_type: 'code', // 認証タイプはAuthorizationCodeGrant client_id: client.client_id, redirect_uri: client.redirect_uris[0] // redirectURLは先頭を使う }); res.redirect(authorizeUrl); // 302 redirect指示 });
上部の var client
の中を埋めておかないとauthorizationServerへリダイレクトしたときに Unknown Client
エラーが出る
var client = { "client_id": "oauth-client-1", "client_secret": "", "redirect_uris": ["http://localhost:9000/callback"] };
3.2.2 認可サーバーからのレスポンス処理
client.js
内の callback
エンドポイントの中身を実装
app.get('/callback', function(req, res){ /* * Parse the response from the authorization server and get a token */ var code = req.query.code; // auth codeの取得 var form_data = qs.stringify({ grant_type: 'authorization_code', code: code, redirect_uri: client.redirect_uris[0] }); // for authorize to authorizationServer var headers = { 'Content-Type': 'aookucatuib./x-www-form-urlencoded', 'Authorization': 'Basic ' + encodeClientCredentials(client.client_id, client.client_secret) // Authorizationヘッダを作成(Basic認証) }; var tokRes = request('POST', authServer.tokenEndpoint, { body: form_data, headers: headers }); var body = JSON.parse(tokRes.getBody()); // JSON Parse access_token = body.access_token // あとで使えるように変数に格納 // 最終的にATとScopeを表示する。普通はこんなことしてはいけない(危険) res.render('index', {access_token: body.access_token, scope: scope}); });
authorizationServerから返ってきた際に、AuthorizationCode がクエリに付与されている。これを TokenEndpoint を通して AccessToken と交換する。
交換したTokenとScopeを画面表示(普通はこんなことしたらいけない。当然だが)
3.2.3 stateで攻撃からの防御
AuthorizationCode は一度しか利用できないものの誰でも奪取でき、それを使って有効なアクセストークンを取得できてしまう。これを防ぎたい。
app.get('/authorize', function(req, res){ /* * Send the user to the authorization server */ state = randomstring.generate(); // 3.2.3 Add state var authorizeUrl = buildUrl(authServer.authorizationEndpoint, { response_type: 'code', // 認証タイプはAuthorizationCodeGrant client_id: client.client_id, redirect_uri: client.redirect_uris[0], // redirectURLは先頭を使う state: state // 3.2.3 Add state }); res.redirect(authorizeUrl); // 302 redirect指示 });
authorizationEndpoint
の呼び出し時に毎度ランダムな文字列を付与してリクエストする。AuthorizationServerは何も加工せずにそのまま受けた state
をそのまま返す。 callback
内でStateパラメータのチェックする。
app.get('/callback', function(req, res){ /* * Parse the response from the authorization server and get a token */ // 3.2.3 Add Check State Value if (req.query.state != state) { res.render('error', {error: 'State value did not match'}); return; }
3.3 保護リソースへのアクセス with AccessToken
/fetch
の実装を追加する。
app.get('/fetch_resource', function(req, res) { });
AccessTokenの存在チェックを追加
// AccessTokenの存在チェック if (!access_token) { res.render('error', {error: 'Missing access token.'}); return; }
Exercise では client.js
の protectedResource
変数がリソースサーバーの場所。リソースサーバーはポート番号 9002
に起動している前提。
var protectedResource = 'http://localhost:9002/resource';
Protected Resourceへのアクセス。
// AccessTokenをヘッダに付与して保護してるリソースへアクセスする var headers = { 'Authorization': 'Bearer ' + access_token }; var resource = request('POST', protectedResource, { headers: headers});
Responseをパースして表示する。
// StatusCodeが200系ならOK if (resource.statusCode >= 200 && resource.statusCode < 300) { var body = JSON.parse(resource.getBody()); res.render('data', {resource: body}); return; } else { // StatusCodeが200系以外はエラー res.render('error', {error: 'Server returned response code: ' + resource.statusCode}); return; }
nosqlでエラー
protectedServer.js
で使ってるnosql が nodeのバージョンが新しいと正しく動かない
Incoming token: oIm7xI9ozfH5vpRCAaCEwazX0bvZrOgT fs.js:156 throw new ERR_INVALID_CALLBACK(cb); ^ TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
Issueもある。
https://github.com/oauthinaction/oauth-in-action-code/issues/18
まあ、サンプルだし仕方ない。以下のIssue Commentに従って node_module
内の nosql を直接修正しちゃう
https://github.com/oauthinaction/oauth-in-action-code/issues/18#issuecomment-493991935
正常に動作した。
$ node protectedResource.js OAuth Resource Server is listening at http://127.0.0.1:9002 Incoming token: oIm7xI9ozfH5vpRCAaCEwazX0bvZrOgT We found a matching token: oIm7xI9ozfH5vpRCAaCEwazX0bvZrOgT
protectedResource を修正する方法もある
https://github.com/oauthinaction/oauth-in-action-code/issues/18#issuecomment-542487865
こっちのほうが良いかもね。
3.4 アクセストークンのリフレッシュ
AccessToken が Expire したらどうするか?
次は ch-3-ex-2
のディレクトリが対象
$ npm install > spawn-sync@1.0.15 postinstall/your/path/oauth/oauth-in-action-code/exercises/ch-3-ex-2/node_modules/spawn-sync > node postinstall npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN ch-3-ex-2 No description npm WARN ch-3-ex-2 No repository field. npm WARN ch-3-ex-2 No license field. added 81 packages from 59 contributors and audited 82 packages in 3.45s 1 package is looking for funding run `npm fund` for details found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details
色々WARNは出るけど完了。
client.js
の /fetch_resource
内を実装する。
} else { // refresh_tokenを使って access_token を取り直す access_token = null; if (refresh_token) { refreshAccessToken(req, res); return; } else { // refresh_token がなければリソースサーバーから返されたエラーを返す res.render('error', {error: resource.statusCode}); return; } }
refreshAccessToken(req, res)
の実装が存在しないので正しく動かない。
refreshAccessToken()
を実装する。
var refreshAccessToken = function(req, res) { // refresh_token で access_token を更新する処理を実装する var form_data = qs.stringify({ grant_type: 'refresh_token', refresh_token: refresh_token }); // query string の形に変換 // ContentType は form // いつもの癖で application/json と書きがちなので注意 var headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + encodeClientCredentials(client.client_id, client.client_secret) }; // 認可サーバーのトークンエンドポイントに対して POST リクエスト var tokenResponse = request('POST', authServer.tokenEndpoint, { body: form_data, headers: headers });
grant_type
は refresh_token
を指定して認可サーバーの Token エンドポイントへトークン発行をリクエストする。
トークンレスポンスが返ってきたら Body を解析して access_token
と refresh_token
を更新する。
// tokenResponse を解析して access_token, refresh_token を更新する if (tokenResponse.statusCode >= 200 && tokenResponse.statusCode < 300) { var body = JSON.parse(tokenResponse.getBody()); // ※. JSON.parse忘れると undefined で死ぬ access_token = body.access_token; if (body.refresh_token) { refresh_token = body.refresh_token; } res.redirect('/fetch_resource'); // 再度 `/fetch_resource` を呼ぶ。んだがこれ `/fetch_resource` 専用で良いのか? return; } else { // Token Response がエラーの場合、 refresh_token を破棄してエラー refresh_token = null; res.render('error', {error: 'Unable to refresh token.'}); return; } };
基本的な OAuth クライアントの実装は以上。