はじめに
OAuth2.0を扱っていて1st PartyのSPAの場合とかどうするのが良いのか悩んでいたら出会ったので訳してみました。
原文はコレ
https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-06
訳してみましたというレベルではないくらい原文に忠実ではありませんが、雰囲気重視の人の参考になればいいかなと思います。
概要
この仕様はブラウザーベースのアプリケーションのベストプラクティス
このメモのステータス
- ドラフトなので最長6ヶ月単位くらいで見直される
- 更新されるかもしれないし、置き換えられたり、他のドキュメントによって非推奨になったりし得る
イントロ
- この仕様はブラウザで実行されるアプリケーションにおいてOAuth2.0のAuthorizationフローを実装するための現時点でのベストプラクティス
- ネイティブアプリの話は別口RFC8252で定義されている
- AppAuthという愛称で呼ばれている
- App Authとよく似ているけどブラウザベースのアプリケーション固有の考慮が追加されている
用語
- OAuth
- Browser-based application
- Webブラウザによってダウンロードされて実行されるアプリケーション
- 大抵はJavaScriptで書かれたもの
- SPAとか
Overview
総括「イマドキのブラウザベースアプリのベストプラクティスは認可コードフローwithPKCE」
- OAuth2.0が作られたときには認可コードフローはブラウザベースのJavaScriptアプリケーションでは使えないものとされていた
- same-origin ポリシーに準拠する前提
- 方やOAuth2.0では認可サーバーが別のドメインである前提
- クロスオリジンなPOSTリクエストが必要とされる
- トークンエンドポイントのことを指していると思われる
- だから認可コードフローはできなかった
- 上記がインプリシットフローを定義した理由
- アクセストークンがリダイレクトされたURLの一部として得られる
- クロスオリジンなPOSTは必要ない
- しかしインプリシットフローにはいくつか欠陥がある
- 一般的にはURL中のアクセストークンが奪われる系の攻撃
- ブラウザでのこれらの攻撃については後で解説がある
- その他興味がある人はoauth-security-topicsを見てくれ
- 一般的にはURL中のアクセストークンが奪われる系の攻撃
- 近年ではCORSが当たり前になってきた
- だからフツーに認可コードフローで良いんじゃないか
- クロスオリジンなPOSTでトークンエンドポイント叩いてもさ
- トークンエンドポイントならセキュアだろう?(=かなり意訳)
- さらにPKCEに対応しておけばAuthorizationCodeを盗み見られても対策できるだろう
ブラウザベースアプリケーションのMUST
- OAuth2.0の認可コードフロー + PKCE拡張を使う
- CSRF攻撃対策として以下(要はワンタイムトークン)
- PKCEをサポートした認可サーバー
- OR OAuth2.0のstateパラメータを使う
- OR OIDCのnonceパラメータを使う
OAuth2.0認可サーバーのMUST
- リダイレクトURIのマッチングは完全一致で確認すべし(ダメ前方一致)
- PKCEをサポートせよ
- リフレッシュトークンを発行する場合、
- 使用するごとにリフレッシュトークンはローテーション(毎回発行し直せということ)
- リフレッシュトークンには使用期限を設定せよ
1st-Party アプリケーション
- 元々OAuthは3rd-partyアプリケーションがユーザーに代わってAPIにアクセスするためのもの
- でも1st-partyでも同じ様に有用
- 1st-partyアプリケーションはAPIとアプリケーションの提供元組織が同じという意味
- XXX銀行APIを使う、XXX銀行公式アプリみたいなもの
- 1st-partyアプリケーションはAPIとアプリケーションの提供元組織が同じという意味
- 1st-partyであっても認可コードフローのようなリダイレクトベースのフローにすべき
(「ような」って言っても結局認可コードそのものを使えって話)- Resource Owner Password Grantは使うべきではない
- 認可サーバーがユーザー認証するときに2FAの画面を出したりできる
(Resource Owner Password Grantだと無理) - 他のIdPを使ったSSOも実現できる
- 認可サーバーがユーザー認証するときに2FAの画面を出したりできる
- Resource Owner Password Grantは使うべきではない
ブラウザベースのアプリケーションのアーキテクチャパターン
- JavaScriptアプリが同一サイトCookieなどでリソースサーバーとゴネゴネできるケース
- (要するに1st-partyでリソースサーバーとフロントエンドは同じドメインを想定していると思われる)
- JavaScriptアプリがそいつ用のバックエンドを持っていて、そいつがリソースサーバーとアクセスするやつ
- (トラディショナルなWebApp+JSフロントみたいな感じで、ASP.Net+AngularとかSpring Boot+Reactとか)
- 純粋なJavaScriptアプリ(SPAかな)で、直接リソースサーバーにアクセスするやつ
1個めのパティーン
OAuthはそもそも3rd-partyアプリ用に作られたので、フロントエンドとリソースサーバーが同じドメインの場合にはベストとは言えないかもしれないが、OAuthを使っておくと後からドメインが追加になったときなどにつぶしが利くので結局のところOAuthを使っておいたほうがいい(と言っている気がする、なんとなく弱気な印象)
- このパターンではOAuthは追加で別のソリューションが使える
- リダイレクトベースのフローはリダイレクトベースの攻撃を許してしまうが、
- 認可サーバーとリソースサーバーが同じドメインであればそもそもリダイレクトメカニズムを使う必要がない
(ちょっと何言ってるのかわかんない)
- もう一つ、ブラウザはアクセストークンをセキュアに保存する方法がない(この原稿を書いている日現在では)
- JavaScriptアプリでは取得したアクセストークンをどこかに保存して、APIリクエストする際にそれをとってくる必要がある
- HTTP-only cookieを使えばそもそもJavaScriptはCookieにアクセスできないのでより安全
- 加えてSameSite cookieはCSRF攻撃を防ぐのにも使える
- またはCookieにCSRFトークンを入れるようにしてもいい
2個めのパティーン
このパティーンは要するにアプリケーション・サーバーがConfidential Clientという扱いなので認可コードフローを使うべき。
- (A) 認可コードを使ってアクセストークンをサーバーサイドで要求する
- (B) サーバーサイドでアクセストークンを受け取って保存する
- そのときにブラウザとの間にCookieベースのセッションを確立する
- アクセストークンの保存はサーバーサイドに保存してもいいし、Cookieに保存しても良い
- (C) JavaScriptアプリがリソースサーバーにアクセスしたいときは、サーバーサイドが中継してリクエストしなければならない
- (D) レスポンスも中継して返される
+-------------+
| |
|Authorization|
| Server |
| |
+-------------+
^ +
|(A) |(B)
| |
+ v
+-------------+ +--------------+
| | +---------> | |
| Application | (C) | Resource |
| Server | | Server |
| | <---------+ | |
+-------------+ (D) +--------------+
^ +
| |
| | browser
| | cookie
| |
+ v
+-------------+
| |
| Browser |
| |
+-------------+
- フロントエンドではなくて、アプリケーションサーバがOAuthフローを実行して、アクセストークンやリフレッシュトークンは「伝統的なブラウザCookieを使ったセッション」の仕組みで内部に保存できる
- アプリケーションサーバはConfidential Clientとして考慮されるべきで自身のクライアントシークレットを保持する
- ブラウザとアプリケーションサーバ間のセキュリティに関しては範囲外だから https://cheatsheetseries.owasp.org/ でも見とけ。
- CookieのセッションIDとしてアクセストークンそのもの使う場合、エンドユーザーにアクセストークンそのものを見せちゃうから注意(JSアプリを使っていないときもブラウザに残る)
3個めのパティーン
このパティーンは要するにPublic Clientだから安全にクライアントシークレットを保持できない。故にクライアント認証の安全なメカニズムもない。
- (A) JavaScriptのコードはスタティックなウェブホストからブラウザーにダウンロードされる
- (B) ブラウザ上のコードが認可コードフロー+PKCEでアクセストークンを取得する
- (C) トークンAPI(POST)のレスポンスとしてアクセストークンが得られる
- (D) アクセストークンを付加してリソースサーバーにリクエストする
- (E) レスポンスがもらえる
+---------------+ +--------------+
| | | |
| Authorization | | Resource |
| Server | | Server |
| | | |
+---------------+ +--------------+
^ + ^ +
| | | |
|(B) |(C) |(D) |(E)
| | | |
| | | |
+ v + v
+-----------------+ +-------------------------------+
| | (A) | |
| Static Web Host | +-----> | Browser |
| | | |
+-----------------+ +-------------------------------+
- このシナリオでは認可サーバーとリソースサーバーはCORSをサポートしていないといけない
認可コードフロー
- パブリックなブラウザベースのアプリの認可コードフローではここで述べる追加の要件に従う必要がある
認可リクエストをブラウザからするとき
- PKCEに対応しておく必要がある
- PKCEは認可コード奪取攻撃の対策
認可コードのリダイレクトを処理するとき
- 認可サーバーは完全一致で登録されたリダイレクトURIのチェックをすべし
リフレッシュトークン
-
リフレッシュトークンはアクセストークンの期限が切れたときにアクセストークンの再取得ができる方法
-
Public Clientではアクセストークンが漏洩するよりもリフレッシュトークンが漏洩するリスクのほうが大きい
- 攻撃者が継続的にリフレッシュトークンで新しいアクセストークンを取得し続けることができるから
-
認可サーバーとアプリはこのリフレッシュトークンに関してこのBCPのMUSTに準拠すべき
-
またoauth-security-topicsのリフレッシュトークン周辺のおすすめにも従うべき
-
特に認可サーバーは以下に従うべき
- リフレッシュトークンと使われるたびにローテーションされるべき
- 使用期限が設定されるべき
- ローテーションされるときにリフレッシュトークンの期限は延長されるべきではない
セキュリティ的考慮
- ブラウザベースのアプリケーションはPublic Clientとして定義されていて、認可サーバーに登録されるべき
- 認可サーバーはクライアントタイプを記録してそれを識別すべき
クライアント認証
- ブラウザベースのアプリのソースコードはブラウザーに配信されてしまうので安全にシークレットを持つことはできない、だからPublic Clientの扱いになる
- だれかがソースコードを解析してクライアントシークレットを引っこ抜いてばらまくこともできる
- クライアントシークレットでは認証できずなりすましも可能
クライアントのなりすまし
- 認可サーバーは認可リクエストの処理をユーザーのconsent抜きで自動で行ってはならない
- クライアントのアイデンティティが保証されているなら別だけど
(Public Clientではなりすまされるので無理と言っておいてなんでこの一文をかいたのか不明)
- クライアントのアイデンティティが保証されているなら別だけど
CSRF対策
- クライアントは認可サーバー毎に一意なリダイレクトURIを使う必要がある
- クライアントはリダイレクトURIをstateのようなセッションデータとともにほぞんする必要がある
- そして認可レスポンスを受け取ったときにそのURIを完全一致で検証する必要がある
クロスドメインリクエスト
- 認可サーバーはCORSをサポートする必要がある
- この仕様にはどのようなCORSポリシーにすべきかは含まれていない
Content-Security Policy
- 長期間寿命のリフレッシュトークンを使いたい場合、または特権スコープを使いたい場合Content Security PolicyのようなメカニズムでJavaScriptの実行を制限すべき
OAuth Implicit Flow
Implicit Flowの脆弱性などをずらずらと並べているが割愛
追加のセキュリティ考察
- OWASP見とけ!