Skip to content

필사 모드: OpenID Connect ディープダイブ — Authorization Code Flow から Discovery まで

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

2026 年現在、新しく作られるほぼすべてのシステムのログインは OpenID Connect(OIDC)に収斂しました。ソーシャルログインも、社内 SSO も、モバイルアプリも、CLI ツールのログインでさえ、その底には OIDC Authorization Code Flow が流れています。それにもかかわらず、OIDC を「ライブラリの設定数行」としてしか知らないまま運用し、トークン検証の欠落や nonce の未使用といった穴から事故が起きる事例が後を絶ちません。

本記事は OIDC をプロトコルレベルで解剖します。HTTP リクエスト/レスポンスを直接読み、3 種類のトークンの役割を区別し、Discovery と JWKS の動作を理解し、最後に実務用のトークン検証チェックリストとしてまとめます。OAuth 2.1 ドラフトが事実上の基準となった時点ですので、すべての説明は Code + PKCE を前提とします。

OIDC のレイヤー構造 — OAuth 2.0 の上に積んだもの

OIDC は OAuth 2.0 を置き換えません。**OAuth 2.0 のフローをそのまま使いながら、その上に認証レイヤーを追加**します。

+---------------------------------------------------------------+

| OIDC が追加したもの |

| - ID Token(署名付き JWT、認証結果の標準フォーマット) |

| - scope=openid と標準クレーム(sub, email, name...) |

| - Discovery (/.well-known/openid-configuration) |

| - JWKS(署名鍵の配布)、UserInfo エンドポイント |

| - nonce, max_age, prompt, acr など認証専用パラメータ |

| - RP-Initiated / Back-Channel Logout |

+---------------------------------------------------------------+

| OAuth 2.0 (RFC 6749) が提供するもの |

| - authorize / token エンドポイントと grant フロー |

| - access token, refresh token |

| - クライアント登録と redirect_uri の検証 |

+---------------------------------------------------------------+

| 基盤: HTTP + TLS, JWT (RFC 7519), JWS/JWK |

+---------------------------------------------------------------+

用語も OAuth の上に重ねて変わります。OAuth の client は OIDC では **RP(Relying Party)**、authorization server は **OP(OpenID Provider)**になります。

3 つのトークン — 役割を絶対に混ぜないこと

| 項目 | ID Token | Access Token | Refresh Token |

| --- | --- | --- | --- |

| 受け手(audience) | RP(クライアント) | Resource Server(API) | OP の token エンドポイント |

| 目的 | 「ユーザーが認証された」ことの証明 | API 呼び出しの権限 | 新しいトークンの再発行 |

| フォーマット | 必ず JWT | 自由(JWT または opaque) | 自由(通常 opaque) |

| 検証の主体 | RP 自身が署名/クレームを検証 | API サーバーが検証 | OP のみが解釈 |

| 寿命 | 短く(分単位) | 短く(5〜15 分推奨) | 長く(ローテーション必須) |

| 送ってはいけない先 | API に送らない | ログインの証拠に使わない | RP の外へ絶対に出さない |

最も多い 2 つの誤用:

1. **ID Token を API に Bearer として送ること** — ID Token の aud は client_id であって API ではありません。API がこれを受け入れるなら audience 検証を放棄したことになります。

2. **Access Token でログイン処理をすること** — access token には「誰が認証したか」についての受け手に紐づく保証がありません。ログインの判断は ID Token だけで行います。

ID Token の中身を覗く

ID Token は 3 つの部分(header.payload.signature)からなる JWS です。

{

"alg": "RS256",

"typ": "JWT",

"kid": "rsa-key-2026-05"

}

{

"iss": "https://idp.corp.com/realms/prod",

"sub": "f9a8b7c6-1234-5678-90ab-cdef12345678",

"aud": "web-dashboard",

"exp": 1781258400,

"iat": 1781258100,

"auth_time": 1781258095,

"nonce": "n-0S6_WzA2Mj",

"acr": "urn:mace:incommon:iap:silver",

"azp": "web-dashboard",

"email": "yj.kim@corp.com",

"email_verified": true,

"name": "Youngju Kim",

"preferred_username": "yj.kim"

}

- iss: 発行者。Discovery ドキュメントの issuer と**文字列として正確に**一致しなければなりません。

- sub: ユーザーの不変の識別子。**アカウントの突合は email ではなく iss+sub の組み合わせ**で行うべきです。メールアドレスは変わり、再利用され、未検証の可能性もあります。

- aud / azp: このトークンの受け手(自分の client_id)。他アプリのトークンを拒否する根拠です。

- exp / iat / auth_time: 失効、発行、実際の認証時刻。auth_time は max_age の要求とともに再認証ポリシーに使われます。

- nonce: 認証リクエスト時に送った値のエコー。リプレイ防御の核心です。

Authorization Code Flow + PKCE — HTTP レベルの解剖

全体のシーケンスは次のとおりです。

[ブラウザ] [RP サーバー] [OP]

| | |

|-- 1. GET /login ----->| |

| | 2. state, nonce, |

| | code_verifier 生成 |

|<- 3. 302 OP /authorize へリダイレクト --------|

|-- 4. GET /authorize ------------------------->|

|<- 5. ログイン UI(SSO セッションがあれば省略)|

|-- 6. 認証(passkey/MFA) -------------------->|

|<- 7. 302 redirect_uri?code=...&state=... -----|

|-- 8. GET /callback?code=...&state=... ->| |

| | 9. state を照合 |

| |-- 10. POST /token --->|

| | (code+verifier) |

| |<- 11. トークン応答 ---|

| | 12. id_token 検証、 |

| | nonce 照合 |

|<- 13. セッションクッキー --| |

ステップ 1: 認証リクエストの作成

RP はリクエスト前に 3 つのランダム値を作ります。

state: CSRF 防御用(セッションに保存)

state=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)

nonce: ID Token のリプレイ防御用(セッションに保存)

nonce=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)

PKCE: code_verifier とその SHA-256 ハッシュである code_challenge

code_verifier=$(openssl rand -base64 64 | tr -d '=+/' | cut -c1-128)

code_challenge=$(printf '%s' "$code_verifier" \

| openssl dgst -sha256 -binary | openssl base64 -A \

| tr '+/' '-_' | tr -d '=')

そしてブラウザを authorization エンドポイントへ送ります。

GET /realms/prod/protocol/openid-connect/auth

?response_type=code

&client_id=web-dashboard

&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback

&scope=openid%20profile%20email

&state=af0ifjsldkj

&nonce=n-0S6_WzA2Mj

&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

&code_challenge_method=S256 HTTP/1.1

Host: idp.corp.com

ステップ 2: コールバックと code の受信

認証が終わると OP はブラウザを送り返します。

HTTP/1.1 302 Found

Location: https://app.example.com/callback

?code=SplxlOBeZQQYbYS6WxSbIA

&state=af0ifjsldkj

&iss=https%3A%2F%2Fidp.corp.com%2Frealms%2Fprod

RP が直ちにやるべきこと: **セッションの state と戻ってきた state が一致するかを比較**します。不一致ならその場で中断します(CSRF の試み)。iss パラメータ(RFC 9207)があれば、期待した発行者かどうかも確認して mix-up 攻撃を遮断します。

ステップ 3: code をトークンに交換(back-channel)

POST /realms/prod/protocol/openid-connect/token HTTP/1.1

Host: idp.corp.com

Content-Type: application/x-www-form-urlencoded

Authorization: Basic d2ViLWRhc2hib2FyZDpzM2NyM3Q=

grant_type=authorization_code

&code=SplxlOBeZQQYbYS6WxSbIA

&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback

&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

OP は code_verifier を SHA-256 でハッシュし、最初に受け取った code_challenge と比較します。コードが窃取されても verifier なしでは交換が不可能 — これが PKCE(RFC 7636)です。

レスポンス:

{

"access_token": "eyJhbGciOiJSUzI1NiIs...",

"token_type": "Bearer",

"expires_in": 300,

"refresh_token": "eyJhbGciOiJIUzUxMiIs...",

"id_token": "eyJhbGciOiJSUzI1NiIs...",

"scope": "openid profile email"

}

RP は id_token を検証し(下のチェックリスト)、payload の nonce がセッションに保存した値と一致することを確認した後に、はじめてログイン処理を行います。

confidential vs public クライアント

| 区分 | confidential | public |

| --- | --- | --- |

| 例 | サーバーレンダリング Web アプリ、BFF | SPA、モバイル、CLI |

| client_secret | 保有(または private_key_jwt) | なし |

| token リクエストの認証 | Basic または mTLS/JWT | client_id のみ |

| PKCE | OAuth 2.1 ではそれでも必須 | 必須(唯一の防御線) |

OAuth 2.1 基準では PKCE はクライアントの種類に関係なくすべてに適用します。SPA であればトークンをブラウザに直接持たせず、BFF(Backend For Frontend)パターンで confidential クライアント化するのが 2026 年の推奨構成です。

Discovery — 設定をコードに埋め込まない方法

OP は自身のすべての設定を標準パスに公開します。

curl -s https://idp.corp.com/realms/prod/.well-known/openid-configuration

{

"issuer": "https://idp.corp.com/realms/prod",

"authorization_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/auth",

"token_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/token",

"userinfo_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/userinfo",

"jwks_uri": "https://idp.corp.com/realms/prod/protocol/openid-connect/certs",

"end_session_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/logout",

"scopes_supported": ["openid", "profile", "email", "offline_access"],

"response_types_supported": ["code"],

"id_token_signing_alg_values_supported": ["RS256", "ES256", "EdDSA"],

"code_challenge_methods_supported": ["S256"]

}

RP ライブラリは issuer URL を 1 つ設定すれば、残りはここから読み込みます。重要な検証ルールが 1 つあります。**ドキュメント内の issuer 値が、ドキュメントを取得した URL の issuer 部分と一致しなければなりません。**この確認がなければ、悪意ある OP が他者の設定を装う攻撃が可能になります。

Keycloak 26.6 では EdDSA 署名もサポートされているため、新規構築であれば RS256 に加えて ES256/EdDSA の採用も検討に値します。

JWKS と鍵ローテーション

jwks_uri は ID Token の署名検証に使う公開鍵の一覧(JWK Set)を提供します。

{

"keys": [

{

"kid": "rsa-key-2026-05",

"kty": "RSA",

"alg": "RS256",

"use": "sig",

"n": "0vx7agoebGcQSuuPiLJXZpt...",

"e": "AQAB"

},

{

"kid": "rsa-key-2026-06",

"kty": "RSA",

"alg": "RS256",

"use": "sig",

"n": "zNf1a9pXkQpW3mLbV2c...",

"e": "AQAB"

}

]

}

検証の流れ: トークン header の kid → JWKS から同じ kid の鍵を選択 → 署名検証。鍵が 2 つ見えるのは正常です。**鍵ローテーション中は新しい鍵と古い鍵が共存**するからです。

鍵ローテーションを無停止にする運用ルール:

1. OP: 新しい鍵を先に JWKS へ公開し、十分な時間(キャッシュ TTL 以上)が経ってから新しい鍵で署名を開始し、古い鍵で署名されたトークンがすべて失効してから古い鍵を削除します。

2. RP: JWKS をキャッシュしつつ(毎リクエスト照会は禁止)、**未知の kid に遭遇したら一度だけ再取得**するロジックを入れます。ただし再取得に rate limit をかけて DoS を防ぎます。

3. kid のないトークン、JWKS にない kid、alg の不一致はすべて拒否します。

UserInfo、クレーム、スコープ

スコープ → クレームのマッピング

scope は「どのクレームの束が欲しいか」の単位です。

| scope | 解放される標準クレーム |

| --- | --- |

| openid | sub(OIDC リクエストであることの表明、必須) |

| profile | name, family_name, given_name, preferred_username, picture など |

| email | email, email_verified |

| address | address |

| phone | phone_number, phone_number_verified |

| offline_access | refresh token の発行要求 |

UserInfo エンドポイント

クレームは ID Token に載せて受け取るか、access token で UserInfo を呼び出して受け取れます。

curl -s https://idp.corp.com/realms/prod/protocol/openid-connect/userinfo \

-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."

{

"sub": "f9a8b7c6-1234-5678-90ab-cdef12345678",

"name": "Youngju Kim",

"email": "yj.kim@corp.com",

"email_verified": true,

"groups": ["sso-admins", "developers"]

}

注意: **UserInfo 応答の sub が ID Token の sub と一致するかを必ず比較**してください。トークン差し替え攻撃をふるい落とす最後のセーフティネットです。ID Token を軽く保ちたければクレームを UserInfo へ後回しにし、ゲートウェイで一度に使いたければ ID Token に含める、というトレードオフを設計します。

state と nonce — 2 つの異なる盾

混同しやすい 2 つのパラメータの役割を明確に区別します。

| 項目 | state | nonce |

| --- | --- | --- |

| 防御対象 | CSRF(コールバックの偽造) | ID Token のリプレイ/注入 |

| 誰が作るか | RP | RP |

| どこから戻るか | コールバックのクエリパラメータ | ID Token のクレーム |

| いつ検証するか | コールバック受信の直後 | ID Token の検証時 |

| ないと何が起きるか | 攻撃者が自分の code を被害者セッションに注入(セッション固定) | 窃取された ID Token の再利用 |

典型的な攻撃シナリオを 1 つ見てみましょう。state がなければ、攻撃者は自分のアカウントでログイン手続きを進め、コールバック URL(自分の code を含む)を抜き出して被害者にクリックさせます。被害者のブラウザがそのコールバックを踏むと、**被害者のセッションが攻撃者のアカウントでログイン**されます。以降、被害者が入力するデータは攻撃者のアカウントに蓄積されます。state の照合 1 行がこのすべてを防ぎます。

トークン検証チェックリスト(実務用)

ID Token 受信時(RP):

[ ] 1. token エンドポイントの TLS 応答から受け取ったか

(front-channel での受領は禁止)

[ ] 2. JWS header の alg が許可リスト(RS256/ES256/EdDSA)にあるか

- alg=none を拒否、対称鍵(HS256)の混用を拒否

[ ] 3. kid で JWKS から鍵を見つけて署名検証

[ ] 4. iss == Discovery の issuer(文字列の完全一致)

[ ] 5. aud に自分の client_id が含まれる、複数 audience なら azp == 自分の client_id

[ ] 6. exp 未経過、iat が妥当な範囲(clock skew 60〜120 秒許容)

[ ] 7. nonce == セッションに保存した値(使用後は破棄)

[ ] 8. 再認証を要求した場合は auth_time/acr を確認

[ ] 9. アカウント突合は iss+sub の組み合わせで(email 突合は禁止)

Access Token 受信時(Resource Server):

[ ] 1. 署名検証(JWT の場合)または introspection(opaque の場合)

[ ] 2. iss, exp の検証

[ ] 3. aud または resource indicator が「この API」かを検証

[ ] 4. scope が要求された操作を許可しているか確認

[ ] 5. 必要に応じて発行時刻ベースの失効ポリシーを適用

このうち 4 番(iss)と 5 番(aud)を省略した実装が現実には驚くほど多いのです。「署名さえ有効なら通過」は、**どの OP のどのトークンでも通過**という意味です。

ログアウト — OIDC の難しい部分

OIDC は 3 つのログアウトメカニズムを標準化しました。

- **RP-Initiated Logout**: RP が end_session_endpoint へ送り、OP セッションを終了。

- **Front-Channel Logout**: OP が各 RP のログアウト URL を iframe で読み込み。サードパーティクッキーのブロックで信頼性が低下し、廃れつつあります。

- **Back-Channel Logout**: OP が各 RP のバックエンドへ logout token(JWT)を直接 POST。2026 年時点の推奨案です。

Back-channel logout を受けるには、RP が「sid(セッション ID)→ アプリセッション」のマッピングを維持する必要があります。ステートレスな JWT だけでセッションを運用するとこの要求を満たせないという点が、「アプリセッションはサーバー側セッション + IdP トークンはバックエンドで保管」という BFF 構成を推すもう 1 つの理由です。

OAuth 2.1 時代の OIDC — 2026 年の整合性まとめ

OAuth 2.1 ドラフトの制約を OIDC 実装に当てはめると次のようになります。

1. **Implicit/Hybrid で id_token を front-channel で受け取っていたパターンの廃止** — response_type=code への一本化。form_post が必要な特殊ケースでない限り、fragment でトークンを受け取りません。

2. **PKCE の全面適用** — OIDC の nonce があっても PKCE は別途必要です。nonce は ID Token の注入を、PKCE は code の窃取を防ぎます。防御対象が異なります。

3. **refresh token のローテーション** — public クライアントはローテーション + 再利用検知を有効にします。再利用が検知されたらトークンファミリー全体を失効させます。

4. **redirect_uri の完全一致** — ワイルドカード、部分一致はすべて禁止。

5. **Bearer トークンのクエリ文字列での受け渡し禁止** — ヘッダーまたは POST body のみ。

さらに高セキュリティのドメインであれば FAPI 2.0 Security Profile(Keycloak 26.6 で Final 対応)を、シークレット管理の強化が必要なら client_secret の代わりに private_key_jwt や mTLS クライアント認証を検討します。JWT Authorization Grant や federated client authentication といった Keycloak 26.6 の新機能は、サービス間の信頼連携シナリオでパスワードレスのクライアント認証を可能にします。

トラブルシューティングノート

| 症状 | よくある原因 | 確認方法 |

| --- | --- | --- |

| invalid_grant(token 交換失敗) | code の再利用、code の失効(数十秒)、redirect_uri 不一致、verifier 不一致 | OP のログで拒否理由を確認 |

| 署名検証の失敗が間欠的 | 鍵ローテーション直後の JWKS キャッシュが古い | 未知 kid の再取得ロジックの有無を点検 |

| ログインの無限ループ | コールバックでセッションクッキーが付かない(SameSite、ドメイン不一致) | Set-Cookie 属性とコールバックのドメインを確認 |

| 間欠的な exp/iat エラー | サーバー間のクロックスキュー | NTP の状態、skew 許容値の設定 |

| state 不一致エラーの多発 | マルチタブでのログイン、セッションストアの TTL が短い | state をタブごとに区別して保存 |

| モバイルだけログイン失敗 | アプリ内ブラウザ(WebView)のブロックポリシー | システムブラウザ(ASWebAuthenticationSession など)を使用 |

おわりに

OIDC を一文で要約すると「OAuth 2.0 の配管の上に、署名付き JWT(ID Token)とメタデータの自動発見(Discovery/JWKS)を載せて認証を標準化したプロトコル」です。実務の観点から核心を 3 つ残します。

- **3 つのトークンの役割を混ぜないでください。**ログインは ID Token、API は Access Token、更新は Refresh Token。それぞれ audience と検証の主体が異なります。

- **検証チェックリストをコードレビューの基準にしてください。**iss/aud/exp/nonce/署名のどれか 1 つでも欠けていれば、その実装は未完成です。

- **OAuth 2.1 の制約(Code + PKCE への一本化、refresh のローテーション、exact redirect_uri)はすでに現在の常識です。**ドラフトの確定を待つ理由はありません。

SAML が昨日の信頼を支えているとすれば、OIDC は今日と明日 — passkeys 統合、AI エージェントの委譲、FAPI 2.0 — が積み上がる場所です。プロトコルの底を理解しておけば、その上のどんな変化も怖くありません。

参考資料

- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)

- [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)

- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)

- [RFC 7636 — Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636)

- [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)

- [RFC 7517 — JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [RFC 9207 — OAuth 2.0 Authorization Server Issuer Identification](https://datatracker.ietf.org/doc/html/rfc9207)

- [OAuth 2.1 Draft (draft-ietf-oauth-v2-1)](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)

- [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html)

- [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-2_0-security-profile.html)

- [RFC 8693 — OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

- [Keycloak Documentation](https://www.keycloak.org/documentation)

- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)

현재 단락 (1/256)

2026 年現在、新しく作られるほぼすべてのシステムのログインは OpenID Connect(OIDC)に収斂しました。ソーシャルログインも、社内 SSO も、モバイルアプリも、CLI ツールのログ...

작성 글자: 0원문 글자: 12,342작성 단락: 0/256