四つのフローとその居場所
一言でいうと
公開クライアントにはauthorization code + PKCE、サーバー間の通信にはclient credentialsを使います。残りのほとんどは、使うべきでない理由があるフローです。
なぜ必要なのか
「Googleログインのボタン1つに、なぜこんなに複雑なプロトコルが必要なのか」という疑問は自然なものです。答えはこうです。ユーザーのパスワードをサードパーティのアプリに渡さずに、そのアプリがユーザーの代わりに限られたリソースにアクセスできるようにしたいのです。
この目的のために複数のフローが作られ、時間が経つにつれて、いくつかは非推奨になりました。
どう動くのか
Authorization Codeが基本であり、標準です。ユーザーが認証サーバーへリダイレクトされてログインし、認可コードを受け取ってアプリに戻り、アプリのバックエンドがそのコードをトークンに交換します。コードはブラウザーのURLに露出しますが、1回限りで、単独では使えないという点が要点です。交換にはクライアントシークレットが必要です。
問題は、SPAとモバイルアプリです。これらはシークレットを安全に保管できません。ソースに入れると誰でも見られます。そのため以前は、Implicitフロー、つまりコードなしでトークンをURLフラグメントで直接受け取る方式が使われていました。トークンがブラウザーの履歴とリファラーに残るという深刻な問題があり、現在は推奨されていません。
その代替がPKCE(Proof Key for Code Exchange)です。クライアントがリクエストのたびにランダムな文字列code_verifierを作り、そのSHA256ハッシュをBase64URLでエンコードしたcode_challengeを、認可リクエストに載せて送ります。後でコードをトークンに交換するときに、元のcode_verifierを提出します。認証サーバーはハッシュを再計算して照合します。攻撃者が認可コードを横取りしても、code_verifierがなければトークンを受け取れません。シークレットなしでも安全になるのです。
Client Credentialsは、ユーザーが介在しないサーバー間の通信用です。バッチ処理、サービスアカウント、内部APIの呼び出しです。ユーザーのコンテキストがないので、subはサービスアカウントになります。
Resource Owner Passwordは、アプリがユーザーのIDとパスワードを直接受け取ってトークンに変えるフローです。OAuthがなくそうとした、まさにそのパターンなので、事実上非推奨になっています。レガシーの移行以外では使いません。
現場での姿
トークンの種類ごとの保存場所と寿命を整理すると、次のようになります。
| トークン | 用途 | 寿命 | 保存場所 |
|---|---|---|---|
| Authorization Code | トークン交換用の1回限り | 数秒 | URLパラメータ(すぐに消える) |
| ID Token | ユーザーの身元の確認 | 5–60分 | クライアント |
| Access Token | APIへのアクセス権限 | 5–60分 | メモリまたはBFF |
| Refresh Token | アクセストークンの更新 | 長い(取り消し可能) | httpOnlyクッキーまたはサーバー |
リフレッシュトークンをlocalStorageに置いてはいけない理由は明確です。XSSによってJavaScriptが読めてしまううえ、リフレッシュトークンは長期間有効なので、盗まれると継続的なアクセスを許してしまいます。
stateパラメータも欠かせません。これがないと、攻撃者が自分のアカウントで発行した認可コードを被害者のコールバックに送り、被害者を攻撃者のアカウントにログインさせることができてしまいます。
ブラウザーアプリでトークンをどこに置くのか
フローを正しく選んでも、受け取ったトークンをどこに保管するかで、再び分かれます。ブラウザーで選べる場所は3つだけで、それぞれ何に弱いかが異なります。
| 置き場所 | 弱い点 | 備考 |
|---|---|---|
localStorage |
XSSでそのまま読まれる | リロードしても残るので便利だが、最も危険 |
| JavaScriptのメモリ | XSSには依然として露出、リロードすると消える | アクセストークンを短時間持つには無難 |
httpOnlyクッキー |
JavaScriptからは読めないが、CSRFを別途防ぐ必要がある | SameSiteと検証が一緒に必要 |
ここから出てきた実務の答えが、BFF(Backend For Frontend)です。トークンをブラウザーにまったく渡さずにサーバーが持ち、ブラウザーにはセッションクッキーだけを渡します。ブラウザーのリクエストをサーバーが受け取り、トークンを付けて後ろに転送する構造です。前の3つの置き場所が抱える問題を一度になくせる代わりに、サーバーの層が1つ増えます。
どちらを選んでもXSSがあればほとんど崩れるという点は、はっきりさせておく必要があります。スクリプトが実行されると、トークンを直接読めなくても、そのページからリクエストを代わりに送れるからです。そのため、トークンの保管場所を悩むのと同じ重みで、コンテンツセキュリティポリシーと入力の処理に気を配る必要があります。
ログアウトも、思ったより単純ではありません。前のモジュールで見たとおり、アクセストークンは取り消されないので、ログアウトは、リフレッシュトークンを無効化して、クライアントが持っているものを捨てることに近いのです。複数のアプリケーションが同じログインセッションを共有している場合、1か所でログアウトしたら残りも片付く必要がありますが、そのための別の規約があり、アプリごとにそれを実装しておかなければなりません。「ログアウトボタンを押したから終わり」ではなく、どこまで片付くのかを設計で決めておくことです。
次の確認で見ること
このモジュールは概念だけを扱います。次のモジュールから、実際のKeycloakでトークンを受け取って中身を調べ、その後でPKCEのフローをスクリプトで完走します。