Twitterアカウントで認証するAPIサーバをどう作るか
Twitterアカウントのみを使って認証を行い、HTTPS経由でWebブラウザのJavaScriptをターゲットにWeb APIを提供するサービスを考える。どのような方式があるか、考えてみた。2016年2月。
あまり人に読ませることを想定していない。OAuthも、1.0/2.0のクライアントを実装したことがあるくらい。OpenID/OpenID Connectとか知識ゼロ。
Twitterアカウントのみを使って認証を行う
よく「認証と認可は違う!」とか、「OAuth 2.0での認可をもって認証とするのは危険である」という言説を見かける。斜め読みすると、そもそも↑でやろうとしていることが無理筋なのでは、という気がしてくるが、世の中にはTwitterで認証っぽいことやってるサイトいっぱいあるよね。あれって全部危険なの!?
という疑問を持ちつつ、「OAuth 認証」を定義しようを読んだ。
「IdP (OAuth Server) 上の Identity 情報にアクセスする API へのアクセス権だけを OAuth Protocol に従って RP (OAuth Client) に渡しましょう。Identity 情報にアクセスする API は、まぁ JSON に user_id とか含めりゃだいたい動くでしょ。」っていうノリでできあがったのが、「OAuth 認証」です。
そう。これこれ。これやりたいんす。
OAuth 2.0のImplicit Flowで得られる認可をもって認証とするのではなく、OpenID Connectにきちんと対応しましょうね、という文章を読んだのだが、TwitterそもそもOAuth 1.0Aだし、そもそもOpenID Connectに対応してないし、今回の要件はImplicit Flow的というよりAuthorization Code Flow的な条件だし、その文書はfacebookやmixiとかにしか触れてないし…と悩んでいた。OpenID Connectは、OAuth 2.0上でIdentity Federationを行う標準化された1手法なのね。
というわけで、『[「OAuth 認証」を定義しよう]』を読んで、やりたいことそのものが致命的に破綻してはいないのだな、と理解した。あとは、実装上のセキュリティホールを作らないように気をつければいい。
また、TwitterにはSign in with Twitterという機能がある。これは、何らかの標準によるのではなく、Twitter独自の実装だ。ユーザ側がアクセス許可を出す画面について、一度許可を出していれば、2回目以降は画面を出さずにすむ。
今回はSign in with Twitterも有効にする。めんどくさくても毎回ユーザ側がアクセス許可を出すようにさせるのもありだろう。ここはトレードオフ。
漏らしてはいけない情報は、Consumer Secret。これが漏れるとアウト。
Sign in with Twitterを無効にして、X-Frame-Options対応ブラウザ側であれば、ユーザがアクセス許可を出すタイミングで気づけば大丈夫。でも普通気づかないと思うので結局Consumer Secret漏れるとアウト。
ここらへんは、TwitterのOAuthの問題まとめを参考にした。
Consumer Secretが漏れちゃった場合、どんな影響があるか。漏洩に気づくことができるのか。影響を軽減できるか。これはまだ結論が出ていない。漏れたことに気づいたらすぐにConsumer Secretを再発行するのがよいが、でもそもそも漏れたことに気づかないんじゃね…という印象。
出来るとすれば、Consumer Secretの定期再取得くらい? あとは、↑のサービスで「大事なとき用のパスワード」でも発行して、個人情報の閲覧・編集やお金払うときとかにはそれを入力してもらうとか?
Web APIを上記認証のもと提供する
こんな選択肢あるじゃろ。選択肢のレベルが揃ってなくて排他的でないのは許せ。
- Cookieにsession idを保存する
- JWT: JSON Web Tokenを発行し、リクエストのAuthorizationヘッダに詰める
- OAuth 2.0 (+ OpenID Connect)のサーバを実装する
APIサーバにおいて、Cookieにsession idを保存するのは基本ナシ…という文章があったので、「なんでだろう…」と不思議がっていた。Cookieにsession idを入れることによるセッション管理って、枯れた技法であり、特に避けるべきと思ってなかったからだ。
その疑問は、Cookies vs Tokens. Getting auth right with Angular.JS を読んで解決した。
なるほど、クロスドメインがデフォルトで非対応(HTML/JavaScriptが別ドメインのCDN配信できねー)、session情報をストレージに入れることによる性能低下、CSRF対策がメンドイ、あたりがCookieを使うデメリットなのね。各種ヘッダを入れたり、なつかしのJSONPを使ったり、そういうめんどさがある。
んで、代替案としてJWT。サーバ側で鍵を使って改ざんをチェックできるトークンで、session情報そのものを含むことができる。ストレージレス!
JWT on Cookieも出来なくはないんだろうけど、上記のCookieのデメリット引きずるのであんまり意味ない。ストレージレスにはできる。
JWTは、暗号化はしなくて検証だけできるJWSと、暗号化もするJWEとがあるが、僕の用途ではJWSで十分。
JWTのマズいところは、サーバ側から「このトークンもう無効でっせ!!」と言えないところ。というわけで、トークンの有効期限を短くすることによって対処することにする。
OAuth 2.0のサーバを実装するという選択、とりあえずはない。登場人物として、ユーザー・僕らのAPIサーバ・そのAPIを使ったサービスの3者がいる。当初はAPIサーバと利用者が同一の予定なので、特にAPI利用者に応じてなにがしかのアクセス権限を制御する必要はない。
将来的にAPIをサードパーティーに解放するときに、JWTの利用は将来的なOAuthの利用を阻害しない。OAuth 2.0で得られるアクセストークンがJWTになってますよ、とかもできるわけで。できるよね?
というわけで、JWT使うことにした。
漏らしてはいけない情報は、JWTの秘密鍵。これ漏れるとJSON改ざん検出できない。ストレージとの合わせ技で改ざん検出は出来るんだろうけど、利点殺すしやりたくないよね。
全体の流れ
全てWebブラウザ上。HTTPS前提。
- サービスのJavaScriptから、APIサーバの特定のエンドポイントを叩いて、認証開始する。開くべきURLがレスポンスで返ってくる。
- 返ってきたURLを開き、ユーザがTwitter APIへのアクセスを許可する。
- APIサーバのエンドポイントがコールバックされるので、内部でaccess tokenを取得する。JWTを作成し、JWTをquery stringに含んだ、サービスのURLにリダイレクトする。サービスのURLは事前に登録しておく。
- サービス内のJavaScriptは、URL内のquery stringにJWTが含まれていたら、ローカルストレージに保存する。
- クライアントは、APIサーバにリクエストを投げるたびにJWTをヘッダに詰める。
- サーバはリクエストヘッダ内のJWTを検証し、APIのレスポンスを返す。
…でいける、かな?
守らなければいけないもの
- Twitterから発酵されたConsumer Secret
- JWTの秘密鍵
守り抜く。漏れた場合のダメージ軽減策は要検討。
なんか穴ありそうな気するんだけど、どうだろね?