PKCEのフローを一段ずつ
一言でいうと
PKCEは、認可コードに「このコードは自分が開始したリクエストのものだ」という証明を付ける仕組みです。シークレットがなくても、コードの横取りが無力になります。
なぜ必要なのか
モバイルアプリでカスタムURLスキームでコールバックを受け取っていた時代には、悪意のあるアプリが同じスキームを登録して認可コードを横取りする攻撃が、実際に可能でした。コードさえあればトークンを受け取れたからです(パブリッククライアントなのでシークレットがないため)。
PKCEは、この問題を解決します。コードを横取りされても、トークンを受け取れないようにするのです。
どう動くのか
フローを順に見ると、次のようになります。
まず、クライアントがランダムな文字列code_verifierを作ります。43文字以上128文字以下で、URLセーフな文字だけを使います。この値は、クライアントだけが知っています。
次に、code_challenge = BASE64URL(SHA256(code_verifier))を計算します。認可リクエストのURLに、code_challengeとcode_challenge_method=S256を載せて送ります。plain方式も規格にはありますが、ハッシュをしないものなので意味がありません。
ユーザーが認証サーバーでログインすると、redirect_uriに認可コードが返ってきます。このときstateも一緒に返ってきますが、クライアントが最初に送った値と同じかどうかを必ず確認しなければなりません。違えば、自分が開始したリクエストではありません。
最後に、トークンエンドポイントに、認可コードと一緒に元のcode_verifierを送ります。サーバーがハッシュを再計算して、保存しておいたcode_challengeと比較します。一致すれば、トークンを渡します。
攻撃者がコードを横取りしたとしても、code_verifierを知らないので、交換に失敗します。
現場での姿
リダイレクトURIの登録で、よく事故が起きます。http://localhost:*やhttps://example.com/*のような広いワイルドカードを登録すると、そのドメインにオープンなリダイレクターが1つでもあれば、トークンが漏れます。正確なパスを登録するのが原則です。
リフレッシュトークンのローテーションも、実務の基本です。更新のたびに新しいリフレッシュトークンを渡し、古いものを無効化します。古いリフレッシュトークンが再び使われたら、盗難を疑ってそのセッション全体を切ります。筆者が勧める組み合わせは、短いアクセストークン(5–15分)とリフレッシュトークンのローテーションです。拒否リストなしでも、実質的な無効化の効果が得られます。
そして、BFFパターンも検討する価値があります。トークンをブラウザーにまったく渡さず、バックエンドが保管して、セッションクッキーだけで通信する方式です。XSSでトークンが盗まれる経路そのものがなくなります。
PKCEが防ぐもの
PKCEなしでauthorization codeフローを使うと、コードがリダイレクトURLに載って届く間に
横取りされる可能性があります。モバイルアプリでは、カスタムスキーム(myapp://)を他のアプリが横取りできて、
特に危険でした。
1. 클라이언트가 무작위 verifier 를 만든다
verifier = base64url(random(32~96 bytes))
2. challenge = base64url(sha256(verifier)) ← 이것만 보낸다
3. 인가 요청에 challenge 를 실어 보낸다
4. 코드를 받아 토큰으로 바꿀 때 verifier 를 보낸다
5. 서버가 sha256(verifier) == challenge 인지 확인
コードを横取りした攻撃者は、verifierを知らないので、トークンに交換できません。ハッシュの 一方向性が、それを保証します。
code_challenge_methodには、必ずS256を使います。plainはverifierをそのまま
送るものなので、何も防げません。
今はWebアプリにもPKCEが推奨されます。以前はパブリッククライアント(モバイル・SPA)だけの ものでしたが、OAuth 2.1はすべてのauthorization codeフローに要求します。
stateとnonceは別のものを防ぐ
3つは混同しやすいのですが、それぞれ別の攻撃を防ぎます。
| 値 | 防ぐもの | どこで確認するか |
|---|---|---|
state |
CSRF(他人のログインを自分のセッションに結びつけること) | リダイレクトで戻ってきたとき |
nonce |
IDトークンのリプレイ攻撃 | IDトークンのクレーム |
| PKCE | 認可コードの横取り | トークン交換のとき |
3つとも使います。stateはセッションに保存しておいて、戻ってきた値と比較し、nonceは IDトークンの中にそのまま載って届くので、それを確認します。
リダイレクトURIは完全一致が必要
最もよくある設定の事故です。認証サーバーに登録したURIとリクエストのURIが、1文字に至るまで 同じでなければなりません。
등록: https://app.example.com/callback
요청: https://app.example.com/callback/ ← 슬래시 하나 차이로 거절
ワイルドカードは使いません。https://app.example.com/*を許可すると、そのドメインの
どんなパスにでもコードを送れてしまい、XSS1つでコードが漏れます。
ローカル開発では、http://localhost:3000/callbackのように、ポートまで登録します。ポートが
変わると登録し直す必要があるので、開発用のポートをチームで固定しておくと楽です。
次のラボですること
PKCEのフロー全体を、curlとスクリプトで完走します。verifierとchallengeを作り、認可リクエストを送り、ログインフォームをPOSTしてコードを受け取り、トークンに交換し、間違ったverifierでは失敗することまで確認します。その後、別のラボで、そのトークンを検証するミドルウェアをFastAPIアプリに組み込みます。