認証認可アーキテクチャの変遷(3)クライアント認証の高度化と段階的移行
本シリーズは5部構成である。
- 第1部: モノリスから認証基盤へ
- 第2部: API Gatewayとマイクロサービス
- 第3部(本記事): クライアント認証の高度化(PKCE / DPoP)と段階的移行戦略
- 第4部: 認可サーバーの内部構造と信頼性設計
- 第5部: 自作認可サーバーのデータモデルと運用設計
第1部・第2部はサーバー側のアーキテクチャを追った。第3部は視点をクライアントに移す。トークンを「発行し検証する」仕組みがどれだけ堅牢でも、トークンが盗まれたら終わりという問題が残っているからだ。近年の仕様(PKCE, DPoP, FAPI)はほぼすべて、この「盗難への耐性」を高める方向に進化している。
Bearerトークンの本質的な弱さ
OAuth 2.0のアクセストークンは既定でBearer(持参人)トークンである(RFC 6750)。Bearerとは「持っている者が正当な所有者とみなされる」という意味で、電車の切符や紙幣と同じモデルだ。誰が出したかは問われない。
この単純さが普及を支えた一方で、脅威モデルは明快である:盗めば使える。盗まれる経路は主に3つ。
- 認可コードの横取り(フロントチャネル)→ PKCEが対処
- 通信・ログ・XSSによるトークン漏洩(発行後)→ 送信者制約(DPoP / mTLS)が対処
- クライアントのなりすまし(トークン要求時)→ クライアント認証の強化が対処
以降、この3つの順に見ていく。
Public Client問題とPKCE
前提: クライアントの2分類
| Confidential Client | Public Client | |
|---|---|---|
| 実体 | サーバーサイドアプリ | SPA、モバイルアプリ、CLI |
| シークレットを保持できるか | できる(サーバー内に秘匿) | できない(バイナリは逆コンパイルされ、JSは全文読める) |
| トークン要求時の認証 | client_secret等で認証 | 認証手段がない |
Public Clientはシークレットを持てない。つまり認可サーバーは「認可コードを持ってきたのが本当にそのアプリか」を確認できない。ここに具体的な攻撃が成立した:認可コード横取り攻撃。モバイルOSのカスタムURLスキーム(myapp://callback)は複数アプリが同じスキームを登録でき、悪意あるアプリがリダイレクトを横取りして認可コードを奪い、自分でトークンに交換できてしまう。
PKCEの仕組み
PKCE(Proof Key for Code Exchange, RFC 7636)はこれを「その場で作った使い捨ての合言葉」で解決する。
ポイントは、フロントチャネルに流れるのはハッシュ値(challenge)だけで、原文(verifier)はトークン交換のバックチャネルで初めて提示されることだ。認可コードを横取りした攻撃者はverifierを知らないため、コードをトークンに交換できない。リダイレクトという傍受されうる経路を、傍受されても無意味なデータしか流れない経路に変えたわけで、小さな仕様ながら設計として非常に美しいと思う。
当初はモバイル向けの拡張だったが、現在はConfidential Clientを含む全クライアントでの使用が推奨され、OAuth 2.1では必須化される。シークレットを持つクライアントでもコード注入攻撃(攻撃者が自分のコードを被害者のセッションに差し込む)への対策として機能するからだ。導入コストはほぼゼロ(主要ライブラリは対応済み)なので、現状PKCEなしの認可コードフローが動いているなら、それが最優先の改善項目である。
SPAのジレンマとBFFパターン
SPAはPKCEでコード横取りには対処できるが、より根深い問題が残る。取得したトークンをブラウザのどこに置いてもXSSから守り切れない(localStorageは論外、メモリ保持でも実行中のXSSには奪われる)。
これに対する現在の有力解がBFF(Backend for Frontend)パターンだ。トークンをブラウザに一切下ろさず、BFFサーバーが保持する。ブラウザとBFFの間は伝統的なセッションCookie(HttpOnly + SameSite)で守る。
面白いのは、これが第1部のStage 0で使っていたセッションCookieへの回帰に見えることだ。実際には「ブラウザ↔サーバーはCookieが最も安全、サーバー↔APIはトークンが最も柔軟」という各区間の最適解を組み合わせた形であって、退化ではない。ただしBFFというサーバーコンポーネントの構築・運用が増えるコストは払うことになる。「SPA + 認証」の設計論は結局、トークンをブラウザに置くリスクとBFFを運用するコストの天秤に帰着すると考えている。
クライアント認証の強化
Confidential Clientがトークンエンドポイントで自分を証明する方法にも強度の階段がある。
| 方式 | 仕組み | 弱点/特徴 |
|---|---|---|
| client_secret_basic / post | 共有シークレットを毎回送信 | シークレットが経路上・ログ・AS側DBに存在する。漏洩面が広い |
| private_key_jwt | 秘密鍵で署名したJWTを提示(公開鍵をASに登録) | 秘密がネットワークに流れない。ASは公開鍵しか持たないのでAS側漏洩でもなりすまし不可 |
| tls_client_auth (mTLS, RFC 8705) | クライアント証明書で認証 | 最強クラス。ただし証明書のライフサイクル管理が重い |
構図は「共有秘密から公開鍵暗号へ」であり、パスワードからWebAuthnへの進化と相似形だ。秘密を共有した時点で漏洩面は2倍になるという原則が通底している。金融グレードのプロファイルであるFAPIがprivate_key_jwtまたはmTLSを要求するのはこのためで、B2B APIを提供する立場なら、新規クライアントにはclient_secretではなくprivate_key_jwtを標準にする価値がある。
送信者制約トークン: DPoP
最後に残った脅威が「発行後のトークン盗難」だ。Bearerである限り、ログ流出・中間者・XSSで漏れたトークンは誰でも使える。これを封じるのが送信者制約(sender-constrained)トークン——トークンを特定の鍵の保持者にしか使えないよう縛る仕組みで、現実的な選択肢は2つある。
- mTLS証明書バインディング(RFC 8705): トークンにクライアント証明書のハッシュを埋め込み、RSがTLS層の証明書と突合する。強力だが、ブラウザやGateway/CDN越しの構成でmTLSを通すのは運用的に厳しい
- DPoP(Demonstrating Proof of Possession, RFC 9449): アプリケーション層で所有証明を行う。mTLSが使えない環境(SPA、モバイル)向けの解
DPoPの仕組み
クライアントは鍵ペアを生成し、リクエストごとにその秘密鍵で署名した使い捨てJWT(DPoP Proof)をヘッダに添付する。
POST /resource HTTP/1.1
Authorization: DPoP <access_token>
DPoP: <DPoP Proof JWT>
DPoP Proofには以下が入る:
| クレーム | 内容 | 防ぐもの |
|---|---|---|
jwk(ヘッダ) | クライアントの公開鍵 | — (検証の基点) |
htm / htu | HTTPメソッドとURI | 別エンドポイントへのProof流用 |
iat + jti | 発行時刻と一意ID | Proof自体のリプレイ |
ath | アクセストークンのハッシュ | 別トークンとの組み合わせ |
一方、アクセストークン側には発行時に公開鍵のハッシュ(cnf.jkt)が焼き込まれる。RSは「トークン内の鍵ハッシュ」と「Proofの署名鍵」が一致するか検証する。結果、トークンだけ盗んでも、対応する秘密鍵がなければ使えない。トークンが切符から「顔写真付き定期券」になったと言える。
DPoPの限界と位置づけ
正直に書くと、DPoPは銀の弾丸ではない。
- 鍵まで盗まれたら無力。SPAでは鍵をWebCryptoの
extractable: falseで守れるためXSSでの鍵持ち出しは防げるが、XSSが実行中にその場でProofを作らせる攻撃は防げない。DPoPが縮めるのは「盗んだトークンを後で・別の場所で使う」攻撃面である - サーバー側の実装負荷:
jtiのリプレイ検査には状態管理が要り、時刻ずれ対応でnonceの仕組み(ASがDPoP-Nonceを配る)も必要になる。「ステートレスなJWT検証」の簡潔さは一部失われる - 対応状況はAS・RS・ライブラリともまだ発展途上で、エコシステムの成熟を見ながらの採用になる
したがって適用判断は「守る資産の価値」次第だ。金融API、長寿命のリフレッシュトークンを持つPublic Client(モバイルアプリ)では投資対効果が高い。社内の一般的なCRUD APIに一律適用するのは、現時点では過剰だと思う。
全体まとめ: アーキテクチャパターン × 課題 × 難易度
シリーズ全体を1枚に圧縮する。
| Stage | パターン | 解決する課題 | 技術難易度 | 組織難易度 |
|---|---|---|---|---|
| 0 | モノリス + セッション | (単一アプリなら十分) | 低 | 低 |
| 1 | IdP導入 + OIDC | SSO、パスワード集中管理、MFA一元化、監査 | 中 | 中〜高 |
| 1.5 | PKCE必須化 | 認可コード横取り、コード注入 | 低 | 低 |
| 2 | API Gateway集約 | 検証実装のばらつき、横断ポリシー、監査 | 中 | 中 |
| 2.5 | Phantom Token | 外部への情報漏洩と即時失効とJWT性能の両立 | 中 | 低 |
| 3 | Token Exchange / サービス間認証 | Confused Deputy、内部の最小権限 | 中〜高 | 中 |
| 3.5 | メッシュ + PDP/PEP | ワークロードID、ポリシー中央管理 | 高 | 高 |
| 4 | private_key_jwt / mTLS | クライアントなりすまし、共有秘密の漏洩面 | 中 | 中 |
| 4.5 | DPoP / 証明書バインディング | 盗難トークンの再利用 | 高 | 中 |
| 補 | BFF | ブラウザでのトークン保護不能問題 | 中 | 低 |
眺めると気づくのは、費用対効果はStageの順に並んでいないことだ。PKCE(1.5)はほぼ無料で効果が大きく、DPoP(4.5)は高価で効果が特定的。番号は歴史的な変遷の順であって、投資の優先順位ではない。
段階的移行はどう進めるべきか
最後に、レガシーな状態からの現実的な移行順序を示す。原則はストラングラーパターン——ビッグバン移行を避け、旧系を絞め殺しながら段階的に置き換える——である。
推奨する順序
- IdPを立て、新規アプリから接続する(既存には触らない)。IdPの運用を安定させることが最優先。この時点でPKCEを標準にしておく
- 既存モノリスをRP化する。ログイン画面をIdPリダイレクトに差し替え、ユーザーDBを移行する。全工程で最も泥臭く、最も時間を見積もるべき箇所。レガシーで改修不能なアプリには認証プロキシを被せる
- API GatewayをJWT検証だけの薄い状態で入れる。最初から多機能にしない。ルーティング + 認証 + ログの3点で稼働実績を作る
- モノリス分割とサービス間認証を並行で。分割で切り出した新サービスからClient Credentials / aud検証を規約化する。旧モノリス内部の呼び出しは無理に触らない
- リスクの高い箇所にだけ高度な対策を足す。外部公開APIにPhantom Token、パートナー連携にprivate_key_jwt、金融的操作にDPoP——全面適用ではなく資産価値ベースで
移行でよくある失敗
- 一斉切り替えを狙う: 認証は全機能の前提なので、失敗時の爆風が全域に及ぶ。必ず新旧並行期間を設け、アプリ単位で切り替える
- セッションとトークンの寿命設計を後回しにする: 「IdPセッション」「アプリセッション」「アクセストークン」「リフレッシュトークン」の4つの寿命の整合を最初に設計しないと、「勝手にログアウトされる」「ログアウトしたのに入れる」がユーザー起点で発覚する
- 統制を先に完成させようとする: 第2部で述べた通り、従うコスト > 回避コストの統制は回避される。ガードレールとセルフサービスを統制とセットで出荷する
- 最新仕様から入る: DPoPやメッシュはStage 1〜2の基礎ができて初めて意味を持つ。土台のないところに屋根は載らない
判断のための問い
移行計画を立てるとき、自分ならこう問う。
- いま実際に起きている(起きうる)インシデントは、上の表のどの課題か?
- その課題を解決する最小のStageはどれか?
- そのStageの組織的前提(専任チーム、運用体制)は満たせているか?
この3つに答えられないままアーキテクチャだけ先行させるのが、一番よくある失敗パターンだと思う。認証認可のアーキテクチャは「どこまで行けるか」ではなく「どこで止まるのが自分たちにとって正しいか」を決める営みである。歴史的変遷を知る価値は、先端に追随するためではなく、自分たちの現在地と課題を歴史の中に位置づけて、次の一手だけを正しく選ぶためにあるのだと考えている。
第4部(認可サーバーの内部構造と信頼性設計)では視点を認可サーバーの内側に移し、認証とトークン発行の分離パターン、可用性・鍵管理の設計、FAPIやConformance Suiteといった品質の物差しを扱う。ASを選定・構築・運用する立場のための補遺である。
