logoChibiham
cover
🔐

認証認可アーキテクチャの変遷(3)クライアント認証の高度化と段階的移行

本シリーズは5部構成である。

第1部・第2部はサーバー側のアーキテクチャを追った。第3部は視点をクライアントに移す。トークンを「発行し検証する」仕組みがどれだけ堅牢でも、トークンが盗まれたら終わりという問題が残っているからだ。近年の仕様(PKCE, DPoP, FAPI)はほぼすべて、この「盗難への耐性」を高める方向に進化している。

Bearerトークンの本質的な弱さ

OAuth 2.0のアクセストークンは既定でBearer(持参人)トークンである(RFC 6750)。Bearerとは「持っている者が正当な所有者とみなされる」という意味で、電車の切符や紙幣と同じモデルだ。誰が出したかは問われない。

この単純さが普及を支えた一方で、脅威モデルは明快である:盗めば使える。盗まれる経路は主に3つ。

  1. 認可コードの横取り(フロントチャネル)→ PKCEが対処
  2. 通信・ログ・XSSによるトークン漏洩(発行後)→ 送信者制約(DPoP / mTLS)が対処
  3. クライアントのなりすまし(トークン要求時)→ クライアント認証の強化が対処

以降、この3つの順に見ていく。

Public Client問題とPKCE

前提: クライアントの2分類

Confidential ClientPublic 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 / htuHTTPメソッドとURI別エンドポイントへのProof流用
iat + jti発行時刻と一意IDProof自体のリプレイ
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モノリス + セッション(単一アプリなら十分)
1IdP導入 + OIDCSSO、パスワード集中管理、MFA一元化、監査中〜高
1.5PKCE必須化認可コード横取り、コード注入
2API Gateway集約検証実装のばらつき、横断ポリシー、監査
2.5Phantom Token外部への情報漏洩と即時失効とJWT性能の両立
3Token Exchange / サービス間認証Confused Deputy、内部の最小権限中〜高
3.5メッシュ + PDP/PEPワークロードID、ポリシー中央管理
4private_key_jwt / mTLSクライアントなりすまし、共有秘密の漏洩面
4.5DPoP / 証明書バインディング盗難トークンの再利用
BFFブラウザでのトークン保護不能問題

眺めると気づくのは、費用対効果はStageの順に並んでいないことだ。PKCE(1.5)はほぼ無料で効果が大きく、DPoP(4.5)は高価で効果が特定的。番号は歴史的な変遷の順であって、投資の優先順位ではない。

段階的移行はどう進めるべきか

最後に、レガシーな状態からの現実的な移行順序を示す。原則はストラングラーパターン——ビッグバン移行を避け、旧系を絞め殺しながら段階的に置き換える——である。

推奨する順序

  1. IdPを立て、新規アプリから接続する(既存には触らない)。IdPの運用を安定させることが最優先。この時点でPKCEを標準にしておく
  2. 既存モノリスをRP化する。ログイン画面をIdPリダイレクトに差し替え、ユーザーDBを移行する。全工程で最も泥臭く、最も時間を見積もるべき箇所。レガシーで改修不能なアプリには認証プロキシを被せる
  3. API GatewayをJWT検証だけの薄い状態で入れる。最初から多機能にしない。ルーティング + 認証 + ログの3点で稼働実績を作る
  4. モノリス分割とサービス間認証を並行で。分割で切り出した新サービスからClient Credentials / aud検証を規約化する。旧モノリス内部の呼び出しは無理に触らない
  5. リスクの高い箇所にだけ高度な対策を足す。外部公開APIにPhantom Token、パートナー連携にprivate_key_jwt、金融的操作にDPoP——全面適用ではなく資産価値ベースで

移行でよくある失敗

  • 一斉切り替えを狙う: 認証は全機能の前提なので、失敗時の爆風が全域に及ぶ。必ず新旧並行期間を設け、アプリ単位で切り替える
  • セッションとトークンの寿命設計を後回しにする: 「IdPセッション」「アプリセッション」「アクセストークン」「リフレッシュトークン」の4つの寿命の整合を最初に設計しないと、「勝手にログアウトされる」「ログアウトしたのに入れる」がユーザー起点で発覚する
  • 統制を先に完成させようとする: 第2部で述べた通り、従うコスト > 回避コストの統制は回避される。ガードレールとセルフサービスを統制とセットで出荷する
  • 最新仕様から入る: DPoPやメッシュはStage 1〜2の基礎ができて初めて意味を持つ。土台のないところに屋根は載らない

判断のための問い

移行計画を立てるとき、自分ならこう問う。

  • いま実際に起きている(起きうる)インシデントは、上の表のどの課題か?
  • その課題を解決する最小のStageはどれか?
  • そのStageの組織的前提(専任チーム、運用体制)は満たせているか?

この3つに答えられないままアーキテクチャだけ先行させるのが、一番よくある失敗パターンだと思う。認証認可のアーキテクチャは「どこまで行けるか」ではなく「どこで止まるのが自分たちにとって正しいか」を決める営みである。歴史的変遷を知る価値は、先端に追随するためではなく、自分たちの現在地と課題を歴史の中に位置づけて、次の一手だけを正しく選ぶためにあるのだと考えている。


第4部(認可サーバーの内部構造と信頼性設計)では視点を認可サーバーの内側に移し、認証とトークン発行の分離パターン、可用性・鍵管理の設計、FAPIやConformance Suiteといった品質の物差しを扱う。ASを選定・構築・運用する立場のための補遺である。