LabHub

ブログ

認証と認可 完全ガイド: 仕様原文で正す10の誤解

한국어English日本語

はじめに

このブログにはすでに OAuth 2.0完全攻略JWTとセッション、何をいつ、そしてフレームワーク別の SSO シリーズがあります。それらが説明するのはプロトコルがどう動くかです。この記事は角度が違います。現場で繰り返される誤解が正確にどの地点で生まれるのかを、仕様の原文で正すことが目的です。

始める前に一つはっきりさせておきます。この記事はレシピではありません。 セキュリティの助言は、前提が付いて初めて意味を持ちます。「Cookie に入れましょう」「JWT を使いましょう」といった文は、前提を省いた瞬間に誤った助言になります。ですから本記事では、推奨ごとにそれが成り立つ条件を併記します。条件が違えば、結論も変わらなければなりません。


1. 認証と認可は別の問題である

この区別は説明のための修辞ではありません。HTTP 自体が二つの状態を別のコードに分けています。RFC 9110 §15.5.2 は 401 を「対象リソースに対する有効な認証資格情報を欠くため、リクエストが適用されなかった」と定義し、§15.5.4 は 403 を「サーバーはリクエストを理解したが履行を拒否する」と定義します。401 は認証の問題、403 は認可の問題です。

401 の名前が Unauthorized であることは歴史的な事故に近いものです。名前は認可に見えますが、意味は認証です。

実務でこの区別が効くのは、クライアントの挙動が変わるからです。

ただし意図的な例外があります。リソースの存在そのものを隠す必要がある場合、403 ではなく 404 を返す選択があります。他人の文書 ID を入れて 403 が返れば「その ID は存在する」という情報が漏れるからです。これは正確な意味の伝達と情報漏えいの最小化のトレードオフであり、どちらが正しいと一般化はできません。決めて、文書に書けばよいのです。


2. OAuth 2.0 は認可フレームワークである — OIDC がその上に載せるもの

最も多く繰り返される誤解です。 OAuth 2.0 をログインのプロトコルとして理解したまま設計を始めると、その後のすべての決定がずれます。

RFC 6749 の題名は The OAuth 2.0 Authorization Framework です。文書は自らを認可フレームワークと規定しており、認証プロトコルだとは述べていません。定義する四つの役割も、アクセス委譲の言葉で書かれています。

役割RFC 6749 §1.1 の定義
リソースオーナー保護リソースへのアクセスを許可できる主体。人であればエンドユーザー
クライアントリソースオーナーに代わって保護リソースを要求するアプリケーション
認可サーバーオーナーを認証し認可を得たうえでアクセストークンを発行するサーバー
リソースサーバー保護リソースをホストし、アクセストークンでリクエストを受けるサーバー

ここでアクセストークンは、「このクライアントがこのリソースにこれだけアクセスしてよい」という証書です。「この人が誰か」という証明ではありません。二つの文は違います。

ですからアクセストークンをそのままログインの証拠として使うパターンは危険です。アクセストークンには「このトークンが自分(クライアント)に対して発行されたものか」をクライアントが確認する標準的な仕組みがありません。別のアプリのために発行されたトークンを自分のサーバーに提出されても、区別できない状況が起こり得ます。

OIDC はまさにこの空白を埋めます。 OpenID Connect Core 1.0 のアブストラクト冒頭はこうです。「OpenID Connect 1.0 は OAuth 2.0 プロトコルの上の単純なアイデンティティレイヤーである。」§2 は ID トークンを「認可サーバーが実施したエンドユーザー認証に関するクレームを含むセキュリティトークン」と定義し、形式は JWT です。

ID トークンの必須クレームは isssubaudexpiat で、aud はそのクライアントの client_id を必ず含みます。この aud の検査こそ、アクセストークンにはない仕組みです。自分の client_id が入っていない ID トークンは自分宛てではないので拒否します。

まとめるとこうなります。


3. 推奨から外れたフローと、その代わりに来たもの

OAuth 2.0 のセキュリティ指針は 2012 年の RFC 6749 に留まっていません。RFC 9700(2025年1月、BCP 240) が現行のセキュリティベストプラクティスであり、RFC 6749・6750・6819 を更新します。ここで二つのグラントの位置づけが変わりました。

3-1. インプリシットグラント — SHOULD NOT

RFC 9700 §2.1.2 はこう記します。「クライアントはインプリシットグラント(レスポンスタイプ token)や、認可レスポンスでアクセストークンを発行する他のレスポンスタイプを使用すべきではない(SHOULD NOT)。ただし認可レスポンスへのアクセストークン注入が防止される場合を除く。」

理由は二つです。アクセストークンが認可レスポンス(フラグメント、リダイレクト、ブラウザ履歴、リファラ)を流れる過程で漏えいとリプレイに晒されること、そして認可レスポンスで発行されたトークンを特定のクライアントに束縛する標準化された送信者制約の方法が存在しないことです。

3-2. リソースオーナーパスワードクレデンシャル(ROPC) — MUST NOT

§2.4 の表現はより強いものです。「リソースオーナーパスワードクレデンシャルグラントは使用してはならない(MUST NOT)。」

理由は、資格情報をそのままクライアントに晒して攻撃面を広げること、そして二要素認証や複数段階の利用者操作を要する認証手順とそもそも噛み合わないことです。

SHOULD NOT と MUST NOT の違いを読み飛ばさないでください。 インプリシットは条件付きの禁止、ROPC は無条件の禁止です。文書に根拠を書くとき、この差が実際に効きます。

3-3. 代わりに来たもの

現行の推奨は認可コードグラント + PKCE です。RFC 9700 §2.1.1 は三つの文で釘を刺します。

同じ節はリダイレクト URI についても要求します。認可サーバーは事前登録された URI と完全一致の文字列比較で照合しなければならず、ネイティブアプリの localhost リダイレクトにおけるポート番号だけが例外です。部分一致や前方一致を許す実装はこの要求に違反しています。

リフレッシュトークンも同様です。§2.2.2 は、パブリッククライアントのリフレッシュトークンが送信者制約(sender-constrained)されるか、ローテーションされなければならないと要求します(MUST)。


4. PKCE — 何を防ぎ、何を防がないのか

RFC 7636 が定義する PKCE が防ぐのは認可コード横取り攻撃(authorization code interception attack) です。クライアントとサーバー間の TLS 区間ではなく、端末内のアプリ間通信のような保護されていない経路で認可コードが奪われ、攻撃者がそれをトークンに交換するシナリオです。

動作は単純です。

クライアント                                    認可サーバー
   |                                              |
   |-- code_verifier 生成(ランダム32オクテット)    |
   |-- code_challenge = BASE64URL(SHA256(verifier))|
   |                                              |
   |--- 認可リクエスト + code_challenge + S256 --->|
   |                                              |  (challenge を保存)
   |<-- 認可コード -------------------------------|
   |                                              |
   |--- トークンリクエスト + code + verifier ----->|
   |                                              |  SHA256(verifier) == challenge ?
   |<-- アクセストークン --------------------------|

code_verifier は 43〜128 文字で、仕様は 32 オクテットの乱数を生成して base64url でエンコードすることを推奨します。code_challenge_methodplainS256 の二つで、S256 がサーバー実装必須(MTI)であり、S256 を使えるクライアントは S256 を使わなければなりませんplain は制約のあるレガシー環境のために残っているだけです。

PKCE が防がないものも明確にすべきです。


5. 三つのトークンを区別する

名前がすべて「トークン」なので一括りにされがちですが、消費者も寿命も異なります。

トークン誰が検証するかどこへ送るかクライアントが中身を読んでよいか
アクセストークンリソースサーバーリソースサーバー(API)いいえ。不透明でも正常
リフレッシュトークン認可サーバー認可サーバーのトークン端点いいえ
ID トークンクライアントどこにも送らないはい。そのためのトークン

ここから最も多い二つの誤りが生じます。

第一に、ID トークンを API 呼び出しの Bearer トークンとして使うこと。 ID トークンはクライアントが「利用者が認証された」事実を確認するためのものです。リソースサーバーに提出するために作られたものではなく、aud が API ではなくクライアントを指すため、リソースサーバーから見れば対象の食い違ったトークンです。

第二に、アクセストークンをパースして利用者情報を取り出すこと。 アクセストークンが JWT である保証はありません。不透明な文字列でも仕様上は正常であり、事業者が形式を変えてもクライアントに抗議する根拠はありません。利用者情報が必要なら、ID トークンのクレームか OIDC Core §5.3 の UserInfo エンドポイントを使うのが契約に沿っています。


6. JWT: デコードは検証ではない

RFC 7519 が定義する JWT は JWS(署名または MAC で保護)JWE(暗号化) です。ここから出発すると誤解が整理されます。

6-1. base64url はエンコードであって保護ではない

JWT のペイロードは誰でも読めます。暗号化ではなくエンコードだからです。JWT のペイロードに機微情報を入れることは、公開したのと同じです。 個人識別番号、内部システムの識別子、認可ポリシーの詳細などを入れないでください。

6-2. デコーダが中身を見せたからといって、そのトークンが有効なわけではない

レビューや障害対応の最中に実際に起きることです。デコーダに貼り付けて subexp がもっともらしいから通過、と判断してしまう。デコードは署名検証と何の関係もありません。 このブログの JWT デコーダ も、署名を検証しないと明記しています。デコーダは中身を読む道具であって、真正性を判定する道具ではありません。

6-3. alg: none

RFC 7519 §6 は、algnone で署名が空文字列の非保護 JWT を定義します。仕様がこれを許すのはセキュリティが外部の手段で提供される場合に限られます。問題は、検証側がこの条件を忘れてトークンの言うとおりに従うときです。攻撃者が署名を消して algnone に変えて送れば、そのサーバーでは認証が消滅します。

6-4. アルゴリズム混同(algorithm confusion)

より巧妙なほうです。サーバーが RS256(公開鍵署名)で発行しているのに、検証関数がトークンの alg ヘッダーを読んでそのまま使うとします。攻撃者が alg を HS256 に変え、サーバーの公開鍵を HMAC の秘密鍵として署名すると、検証コードは同じ公開鍵で HMAC を計算して一致と判定します。公開鍵は公開された値なので、誰でもトークンを偽造できるようになります。

RFC 8725(BCP 225)はこの問題を正面から扱います。

一文に縮めるとこうです。トークンが自ら申告したアルゴリズムを信用しないでください。 検証アルゴリズムはアプリケーションがトークンの外側で固定すべきものです。

6-5. 何を検査するかは仕様が決めてくれない

RFC 7519 において isssubaudexpnbfiatjti はすべて OPTIONAL です。この事実はしばしば逆に読まれます。任意であるとは「検査しなくてよい」ではなく、何を必ず検査するかは仕様ではなくアプリケーションが決めなければならないという意味です。「ライブラリが検証してくれるだろう」という期待が外れるのがここです。既定で有効期限だけを見て発行者と対象を見ない設定は珍しくありません。

最低限この程度はアプリケーションが明示的に決め、コードに残すべきです。

[ ] 署名検証に使うアルゴリズムをコードで固定したか(トークンのヘッダーを読まずに)
[ ] 検証鍵の出所が固定されているか(JWKS URL、鍵ローテーション対応を含む)
[ ] iss が期待する発行者か
[ ] aud が自分を指しているか
[ ] exp / nbf を検査するか、許容する時刻ずれは何秒か
[ ] 必要な場合 jti で再利用を防ぐか

6-6. 取り消しは依然として難しい

署名が有効で期限内なら、そのトークンは有効です。それがステートレス検証の利点であり代価でもあります。ログアウト、権限剥奪、アカウント停止を即座に反映するには、結局サーバー側の状態(ブロックリスト、トークンバージョン、セッション照会)が必要になり、その瞬間に「ステートレス」という利点の相当部分は失われます。

ここに正解はありません。軸はアクセストークンの寿命です。短くすれば取り消しの遅延は減りますが、認可サーバーのトラフィックと障害の結合度が上がります。長くすれば逆です。この選択は、組織が耐えられる「権限剥奪の遅延」を何分と見るかの問題であり、その数字を決めて書き残すことが設計です。


7. トークンをどこに置くか — そしてセッションをどう運用するか

ここで「正解は一つ」と言う記事には注意してください。 双方に費用があり、費用の種類が違うだけです。

7-1. Web Storage(localStorage / sessionStorage)

7-2. Cookie(HttpOnly、Secure、SameSite)

7-3. このトレードオフを正直にまとめると

Cookie はトークンの窃取を難しくし、リクエスト偽造を容易にします。Web Storage はその逆です。 そして決定的に、XSS が存在すればどちらも安全ではありません。 Cookie を読めなくても、攻撃者はそのブラウザから認証済みのリクエストを送れるからです。保存場所の選択は XSS 対策の代わりにはなりません。

OWASP のように Cookie を勧める文書が多数であり、その根拠も明確です。ただしその推奨には前提が付きます。

前提が違えば結論も変わるべきです。前提を書かないセキュリティ助言は、次に読む人にとって危険です。

7-4. セッションを運用するということ

トークンであれセッション ID であれ、運用の規則は似ています。OWASP セッション管理チートシートに沿えば、最低限これだけは必要です。


8. 認可はどこで決まるのか

モデルの話から始めると議論が上滑りします。順番を変えましょう。

8-1. モデルは三つ程度で足りる

三つは排他的ではありません。多くは RBAC で大枠を作り、特定の地点で属性や関係を混ぜます。

8-2. 本当の問題は決定地点である

認可の失敗の相当数は、モデルの選択を誤ったからではなく、判定が起きる場所を誤ったために生じます。

クライアント → [APIゲートウェイ] → [サービス] → [データアクセス層] → DB
                      |                 |               |
                 認証+早期遮断      業務規則の判定    所有権の判定

実務的な規則はこうです。認可判定はデータに最も近い場所で最低一度は必ず起きなければなりません。 前段の判定は早期遮断(性能、利用者体験、ログのノイズ削減)のためのものであり、信頼の根拠ではありません。

8-3. 最も頻繁に抜ける検査

オブジェクトレベルの認可です。認証は通り、ロールも合っているのに、「この利用者がこの特定のリソースへの権限を持つか」を誰も確認していない場合です。パスの識別子を差し替えるだけで他人のデータを読む事故はここから生まれます。一覧照会は所有者条件で絞り込みながら、単件照会で同じ条件を落とすパターンが特に多く見られます。

そして判定は閉じる方向に失敗しなければなりません。権限照会がタイムアウトしたときの既定値が許可なら、認可システムの障害がそのまま全面的な権限開放になります。

拒否理由をどこまで詳しく伝えるかも決定事項です。詳しいほどデバッグは楽になり、情報はより漏れます。内部の管理ツールと公開 API が同じ方針を使う理由はありません。


9. 自前で作るか — この決定の代価

最後に、最もよく言葉を濁される問いです。

認証を自前で実装することは禁じられた行為ではありません。ただし費用の大きな決定であり、その費用は最初のリリースではなくその後に請求されます。 この文はそのまま議事録に書いておくほうがよいものです。

9-1. 自前で作れば一緒に引き受けるもの

ログイン画面とパスワード照合は、この一覧の最初の項目にすぎません。

9-2. 委譲すれば費用が消えるのではなく、種類が変わる

9-3. 判断に実際に使われる軸

どちらを選ぶにせよ、選んだ理由と前提を文書に残してください。 2 年後にこの決定を振り返る人に必要なのは結論ではなく、その時点の前提です。それが本シリーズ第 1 回で扱った設計文書の仕事です。


クイズ: 理解度を確認しましょう

クイズ1: モバイルアプリのレビューで、認可リクエストに response_type=token を使いリダイレクトのフラグメントでアクセストークンを受け取るコードを見つけました。何が問題で、何に変えるべきですか。

正解: インプリシットグラントです。RFC 9700 §2.1.2 が SHOULD NOT と規定しており、認可コードグラント + PKCE に変えるべきです。モバイルアプリはパブリッククライアントなので PKCE は MUST です。

解説: 問題は「古いから」ではありません。アクセストークンが認可レスポンスを流れる過程でブラウザ履歴・リファラ・ログから漏れうること、そして認可レスポンスで発行されたトークンを特定のクライアントに束縛する標準的な送信者制約の方法がないことです。移行の際、クライアント側に PKCE を付けて終わりにしてはいけません。認可サーバーが PKCE ダウングレードを防ぐか、つまり code_challenge なしで始まったリクエストに code_verifier を含むトークンリクエストを拒否するかも確認が必要です。

クイズ2: 障害対応中に同僚が「トークンをデコーダに入れたら sub も合っているし exp も切れていないので、このリクエストは正常だ」と言います。何が間違っていますか。

正解: デコードは検証ではありません。署名を確認していないので、そのトークンを自分たちのサーバーが発行したのかどうか分かりません。

解説: JWT のペイロードは base64url エンコードにすぎないため、誰でも好きな値で作れます。このブログの JWT デコーダを含め、多くのデコーダは署名を検証しないと明記しています。判断に必要なのはペイロードのもっともらしさではなく、署名検証を通過したか、そして issaud が期待値と一致するかです。付け加えると、本番のトークンをデコーダに貼り付ける行為自体が、資格情報を外部ツールに露出させることです。

クイズ3: サーバーは RS256 で JWT を発行します。検証コードはトークンの alg ヘッダーを読み、そのアルゴリズムで検証します。どんな攻撃が可能ですか。

正解: アルゴリズム混同攻撃です。攻撃者が alg を HS256 に変え、サーバーの公開鍵を HMAC の秘密鍵として署名すると、検証コードは同じ公開鍵で HMAC を計算して一致と判定します。

解説: 公開鍵は文字どおり公開された値なので、誰でも有効なトークンを偽造できるようになります。RFC 8725 §3.1 は、ライブラリが呼び出し側にサポートするアルゴリズムの集合を指定させなければならず(MUST)、それ以外を使ってはならず(MUST NOT)、各鍵は正確に一つのアルゴリズムとともに使用されなければならない(MUST)と規定します。原則は一つです。検証アルゴリズムはトークンではなくアプリケーションが決めます。同じ理由で alg: none を受け入れる設定も併せて点検すべきです。

クイズ4: チームがトークンを localStorage から HttpOnly Cookie に移しました。「これで XSS は解決した」がなぜ間違いですか。

正解: 二つあります。第一に、HttpOnly はトークンの値を読むことを防ぐだけで、XSS そのものを防ぎません。攻撃者はそのブラウザから認証済みのリクエストをそのまま送れます。第二に、Cookie に移したことで CSRF の面が新たに生じました。

解説: 保存場所の変更はリスクの種類を変えるのであって、なくすのではありません。Cookie は窃取を難しくする代わりに、ブラウザが自動で付けるためリクエスト偽造の経路を開きます。OWASP も XSS と CSRF が結合するとセッション Cookie がリクエストに含まれる点を併記しています。移行には SameSite の設定、CSRF 対策、そして何より出力エンコーディングや CSP といった XSS 対策が伴わなければなりません。

クイズ5: API ゲートウェイが JWT の署名と有効期限を検証し、通過したリクエストだけを注文サービスへ渡します。注文照会 API に他人の注文 ID を入れるとデータがそのまま返ります。何が抜けていますか。

正解: オブジェクトレベルの認可です。ゲートウェイは「この要求者が認証済みの利用者か」だけを確認し、「この利用者がこの特定の注文の所有者か」は誰も確認していません。

解説: 認証と認可を同じ地点で終えたと錯覚したときに出る典型的な事故です。ゲートウェイの判定は早期遮断のためのものであり、信頼の根拠ではありません。所有権の判定はデータに最も近い場所、つまり注文を実際に照会する層で行われるべきです。一覧照会は所有者条件で絞りながら単件照会で落とすパターンが特に多いため、単件照会の経路を個別に点検するとよいでしょう。

クイズ6: フロントエンドが OIDC ログイン後に受け取った ID トークンを、そのまま API 呼び出しの Bearer トークンとして送っています。なぜ問題ですか。

正解: ID トークンはクライアントが認証の事実を確認するためのトークンであり、リソースサーバーに提出するトークンではありません。aud が API ではなくクライアントの client_id を指すため、リソースサーバーから見れば対象の食い違ったトークンです。

解説: リソースアクセスにはアクセストークンを使うべきです。リソースサーバーがこの状況をそのまま受け入れているなら、そのサーバーは aud の検査をしていないという意味でもあります。これは他のクライアントのために発行されたトークンまで受け入れるという、より広い問題につながります。逆方向の誤りも併せて見てください。アクセストークンをパースして利用者情報を取り出すことも契約違反です。アクセストークンは JWT である義務を負いません。


おわりに

この記事で繰り返した文をもう一度集めるとこうなります。

最後に最も大切なこと。前提のないセキュリティ助言を書くことも、受け取ることもしないでください。 「こうすれば安全です」という文には、ほぼ常に「こういう条件では」が前に省略されています。省略された条件が自分たちの環境で成り立たないとき、その文が残すのは安全だという錯覚だけです。条件を書く習慣ひとつが、この領域で最も安価で最も効果的な防御です。


参考資料


関連記事

完全ガイドシリーズ

コメント

まだコメントはありません。

ログインするとコメントできます