OAuth 徹底入門の演習を解いてく第4章
4章はアクセストークンをどのように使うか。リソースサーバーを実装しながら役割と使い方を見る。
4 シンプルな OAuth の保護対象リソースの構築
4.1 OAuth トークンの解析
- AccessToken を Request に付与する方法は3つ
- 推奨は Authorization ヘッダ
- Body にいれるとリクエストの Body の形式が固定化されてしまい汎用性がなくなる。既存のものがそうなっていてどうしてもヘッダをいじれない場合は選択肢として考える
- クエリパラメータは Referrer による偶発的な露出、意図しないログへの記述などアクセストークンが第三者に漏れる可能性が高いため、推奨しない。
- Authorization ヘッダの評価は大文字小文字を区別しない。以下はすべて Valid なヘッダ情報となる
- Authorization: Bearer 987abcdefgaaaa
- Authorization: bearer 987abcdefgaaaa
- authorization: BEARER 987abcdefgaaaa
- 実装では原則小文字に変換して評価すれば良い
var inToken = null; var auth = req.headers['authorization']; // ヘッダから Authorization を取り出す if (auth && auth.toLowerCase().indexOf('bearer') == 0) { inToken = auth.slice('bearer '.length); // bearer の後ろに一つ空白あり〼 } else if (req.body && req.body.access_token) { // body に AccessToken が入力されている場合 inToken = req.body.access_token; } else if (req.query && req.query.access_token) { // クエリパラメータに AccessToken が入力されている場合 // ログに残ったり、リファラを通して不用意に漏れたりする可能性があるため、やむを得ない場合以外非推奨 inToken = req.query.access_token; }
4.2 データストアにあるトークンとの比較
- このサンプルはリソースサーバーと認可サーバーが同じところにある想定
- なので、認可サーバーがアクセストークンを発行して永続化したデータストアを直接見に行ってる
- 認可サーバーとリソースサーバーが別の場合は、Introspection とかを使って AccessToken の有効性を調べる必要がある
nosql.one(function(token) { if (token.access_token == inToken) { return token; } }, function(err, token) { if (token) { console.log("We found a matching token: %s", inToken); } else { console.log("No matching token was found."); } req.access_token = token; next(); return; }); };
- ここの nosql もサンプルのバージョンが古いので手直し必要
var next = function next(cancel, position) { if (cancel) { fs.close(fd, function(err, result) {}); fd = null; fnCallback(true); return; } self.read(fd, position + size, size, fnBuffer, next); };
fs.close(fd, function(err, result) {});
この部分
すべてのリクエストの処理前に AccessToken を検索する
- 全部のリクエストに Injection してほしい場合
// 全ての処理に先んじて AccessToken の取得とチェックが必要 app.all("*", getAccessToken);
- function ごとに Injection
app.post("/resource", getAccessToken, cors(), function(req, res){ // getAccessToken を追加したことで事前に AccessToken のチェックが入る if (req.access_token) { res.json(resource); } else { res.status(401).end(); } }
4.3 トークンの情報をもとにしたリソースの提供
- 有効な AcccessToken だけで判断していいかというと、もちろんそういうものだけではない
- アクセス権を設定してより細かく制御したい
- ch-4-ex-2を見ていく
var requireAccessToken = function(req, res, next) { if (req.access_token) { next(); } else { res.status(401).end(); } };
- ヘルパー関数が実装済み。問題がなければ次の処理へ渡すようになってる
/words
というパスに対して3つのメソッドが定義されている- get
- post
- delete
- AccessToken の scope をチェックする
if (__.contains(req.access_token.scope, 'read')) { res.json({words: savedWords.join(' '), timestamp: Date.now()}); } else { // ヘッダ内にエラー情報を記述 res.set('WWW-Authenticate','Bearer realm=localhost:9002, error="insufficient_scope", scope="read"'); res.status(403).end(); // アクセス拒否 }
- 上記は get の場合、 post の場合は
write
, delete の場合はdelete
の scope を要求するように実装する。 - 認可画面で明示的にチェックを外せば、scope の存在しない状況は作れる
- ただし、そもそもユーザーに選択すらさせたくない場合は、Client の定義から削れば良い。
client.js
のscope
から対象を削除すればOK
// client information var clients = [ { "client_id": "oauth-client-1", "client_secret": "oauth-client-secret-1", "redirect_uris": ["http://localhost:9000/callback"], "scope": "read write" } ];
delete
だけ削除した場合 ↑
4.3.2 異なるScopeによる異なるデータ
- 一つのエンドポイントで scope によってデータの出し分けを行うことができる
- 情報の種類ごとにアクセス権を割り当て、アクセスした Client がどのアクセス権を持っているかで情報を出し分ける
- 追加課題の
lowcarb
も考えてみる
app.get('/produce', getAccessToken, requireAccessToken, function(req, res) { if (__.contains(req.access_token.scope, 'fruit')) { produce.fruit = ['apple', 'banana', 'kiwi']; } if (__.contains(req.access_token.scope, 'veggies')) { produce.veggies = ['lettuce', 'onion', 'potato']; } if (__.contains(req.access_token.scope, 'meats')) { produce.meats = ['bacon', 'steak', 'chicken breast']; } if (__.contains(req.access_token.scope, 'lowcarb')) { produce.meats = ['chicken breast', 'steak', 'bacon']; produce.veggies = ['lettuce', 'onion']; }
- 肉類は全部炭水化物が少ないんでは・・?という予測(正しいかはわからん)
- 果物は果糖多いから炭水化物あるだろ・・・。多分・・・
- Client の scope に
lowcarb
追加しておかないと選べない
var client = { "client_id": "oauth-client-1", "client_secret": "oauth-client-secret-1", "redirect_uris": ["http://localhost:9000/callback"], "scope": "fruit veggies meats lowcarb" };
4.3.3 異なるユーザーによる異なるデータの取得
- 同じエンドポイントにアクセスするが、異なるユーザーでアクセスした場合、取得できるデータが異なる
- サンプルの写真サービスがその例