com4dc’s blog

Javaプログラマーのはずだけど運用してます

OAuth 徹底入門の演習を解いてく第4章

4章はアクセストークンをどのように使うか。リソースサーバーを実装しながら役割と使い方を見る。

4 シンプルな OAuth の保護対象リソースの構築

4.1 OAuth トークンの解析

  • AccessToken を Request に付与する方法は3つ
    • Authorization ヘッダに Bearer トークンとして付与
    • encoded_form で Body にアクセストークンを付与
    • URL クエリパラメータとしてアクセストークンを付与
  • 推奨は 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.jsscope から対象を削除すれば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 異なるユーザーによる異なるデータの取得

  • 同じエンドポイントにアクセスするが、異なるユーザーでアクセスした場合、取得できるデータが異なる
  • サンプルの写真サービスがその例
    • ユーザーA がサードパーティアプリを認可し、写真サービスへのアクセスを許可した場合に取得できる写真
    • ユーザーB がサードパーティアプリを認可し、写真サービスへのアクセスを許可した場合に取得できる写真
    • 上記は異なるデータとなる
    • ただし、アクセスする写真サービスのエンドポイントはユーザー毎に変わらない(同じエンドポイント)

4.4 まとめ

  • アクセストークンの取得
  • アクセストークンの検証
  • scopeによる権限管理
  • アクセストークン取得後、実際にそのアクセストークンをどのように使って保護リソースにアクセスさせるかの実装。知識としてはあったけど、実装すると色々面倒だなぁと思った次第。