Facebook ログインで認証するまでの手順 (OAuth認証とトークン置換攻撃対策)

過去に Facebook ログインを用いてモバイルアプリのユーザの認証を行った時の手順(今そのサービスは無いorz)。Facebook ログインについて書くけれども、twitter や github を用いて認証する場合にも応用は効くはず。(正直セキュリティ系に自信ないのですが書かないよりは書いたほうがいいかなと、、、)

## 基本的な流れ
下図参照
![Screen Shot 2015-09-03 at 10.25.54 AM.png](https://qiita-image-store.s3.amazonaws.com/0/31970/91ee9afb-6a04-1d23-9f32-d0e54b02c1bf.png)

ユーザが Facebook ログインで Facebook サーバから `facebook_token` を取得。それを自分が管理しているサーバに送ってもらう。送ってもらった `facebook_token` が正しいものかを Facebook サーバに問い合わせ、問題ないことが確認できたら、自分のサービス用のトークン (`my_service_token`) を発行してユーザに送る。それ以降のメッセージすべてに `my_service_token` を含めてもらうことで、自分のサーバは通信相手を確認することができる。すべての通信は HTTPS で保護されているので、なんらかの方法で他人に token を盗まれなければ、基本的にはこれで安全に通信することができる。

## トークン置換攻撃というものがあるらしい。
ここで問題になるのは、`token 置換攻撃`という攻撃手法である。
上記の流れで、ユーザから `facebook_token` を受け取る部分がある。攻撃者がこの `facebook_token` を偽装して、他人の token を利用してきた場合、token の確認処理の部分がしっかりと実装されていないとセキュリティホールになる。
攻撃者は他人の token なんてどうやって手に入れるんだ、ランダムに token の文字列を偽装して送っても(ブルートフォース攻撃)、そんなに頻繁にあたらないでしょう？と思う訳なのですが、これが実は結構簡単に手に入れることができてしまう状況で、自分で何か Facebook ログインを利用するサービスを作ればいいんです。そうすると自分のサーバにユーザの `facebook_token` が送られてくるので、これを保存しておいて、他のサービスの攻撃に利用する、という形で利用できてしまいます。

## 対策はどうするのか
token が正しいか確認する部分をしっかりと実装することで、トークン置換攻撃を対策することが出来ます。Facebook の場合は token の情報を Facebook API を叩くことで確認することが可能です。この時に、単に token に対応するユーザが存在する、ということだけでなく、**その token が自分のサービスに対して発行されたことを確認** する必要があります。後続で書きますが、Facebook の token には `app_id` というFacebook に登録したアプリの id がついています。これを確認することで、`token` が自分のサービスに対して発行されたものか、他のサービスに対して発行されたものか確認することが出来ます。
こうすることで、 他のサービスに登録されている token が自分のサービスに送られてきたとしても、それを不正なものであると判断することが出来ます。
(余談ですが、これは Facebook から発行される token が、同じユーザであってもサービスが異なれば違うものになるからです。サービスが違っていても、同じユーサには同じ token を返す、というような仕様であったとしたら、トークン置換攻撃を防ぐのはかなり難しいのではないかと思います。)

## 実装

Facebook は token の情報を取得するエンドポイントを用意していて

```rb
GET https://graph.facebook.com/debug_token?
  input_token={token-to-inspect}
  &access_token={app-token-or-admin-token}
```
*token-to-inspect* にユーザから送られてきた `facebook_token` を入れて送信することで、↓のようなレスポンスが帰ってきます。

```rb
{"data"=>{
  "app_id"=>"123456789012345",
  "application"=>"MyService",
  "expires_at"=>1440925200,
  "is_valid"=>true,
  "scopes"=>["user_about_me", "public_profile"],
  "user_id"=>"98765321098765"}
}
```
注目すべきは `app_id` の部分です。Facebook ログインを実装するためには事前に Facebook のサイトに自分のアプリを登録する必要があるのですが、この時に `app_id` が割り当てられています。そのため、そのとき割り当てられた id と、レスポンスで返ってきた `app_id` の値が等しいかを確認することで、トークン置換攻撃を判別できるのです。

↓が ruby の gem を用いた参考コード。動作確認さえしていないので参考までに。

```rb
require koala
# Koala というライブラリを利用して判定する例。
@graph = Koala::Facebook::API.new(params[:fb_token])

# https://graph.facebook.com/debug_token? に問い合わせ
token_info = @graph.debug_token(params[:fb_token])

# 期限切れなどになっていないかを確認
fail unless token_info['data']['is_valid']

# Facebook の user_id も確認する
fail unless token_info['data']['user_id'].eql?(params[:fb_user_id])

# app_id は最初にアプリを登録したときにわかっているので固定値
fail unless token_info['data']['app_id'].eql?('123456789012345')
```
注意点ですが、ユーザが Facebook ログインを行うと、 token と一緒に Facebook 上でのユーザ ID も返ってきます。このユーザ ID も、自分がサービスを提供しているサーバに送ってもらいます。これは token だけの確認では、その token がランダムに送られてきてたまたま当たったもの(ブルートフォース攻撃)なのかわからないためです。ユーザ ID と token は十分に長いので、このふたつの組み合わせが一致することは確率的にかなり低くなるので、ブルートフォース攻撃の防止をしています。

## まとめ
以上で Facebook ログインを用いて認証を行う場合の流れを書きました。github や twitter などの認証に関してもこのような流れになるのではと思っています。上記のような攻撃にも強い(?) OpenID Connect のような仕様もあったりするらしく、将来この辺のやり方は変わってくるかもしれません。最後に参考にしたサイトを記述しておくのでそちらも参考にしてください。

## 参考
- [モバイルアプリのユーザ認証方法についてまとめてみた]
(http://qiita.com/ledmonster/items/0ee1e757af231aa927b1)

- [単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる](http://www.sakimura.org/2012/02/1487/)

- [GoogleのOAuth 2.0実装におけるToken置換攻撃の防ぎ方](http://d.hatena.ne.jp/ritou/20120702/1341235859)

- [OAuth 2.0の概要とセキュリティ
](http://www.slideshare.net/charlier-shoe/oauth-20-29540466)

- [Facebook token に関するドキュメント](https://developers.facebook.com/docs/facebook-login/access-tokens#debug)

- [Facebook ログインの security に関するドキュメント](https://developers.facebook.com/docs/facebook-login/security#tokenhijacking)