OpenID Connect

3 行まとめ
Section titled “3 行まとめ”- OpenID Connect は OAuth 2.0 の上に載る認証レイヤ。OIDC Core 1.0 §1 が自身を “a simple identity layer on top of the OAuth 2.0 protocol” と説明する。 OAuth 2.0 が「何をしてよいか」を扱うのに対し、OIDC は**「誰がいつ認証されたか」**を扱う。
- 追加されるものは実質 3 つ。
scopeにopenidを入れる、id_token(署名付き JWT)が返る、 UserInfo エンドポイントが使える。フローそのものは認可コードフローのまま。 - 難所は
id_tokenの検証。 OIDC Core §3.1.3.7 が 13 ステップを定めており、 署名検証はそのうちの 1 つでしかない。aud/nonce/expはクライアント自身が MUST で検証する。
当たるべき文書
Section titled “当たるべき文書”OIDC は RFC ではない。 OpenID Foundation が発行する仕様で、番号ではなく errata set の版で管理されている。
| 文書 | 版・日付 | 扱う範囲 |
|---|---|---|
| OIDC Core 1.0 | errata set 2 / 2023-12-15 | ID トークン、フロー、クレーム、UserInfo |
| OIDC Discovery 1.0 | errata set 2 / 2023-12-15 | /.well-known/openid-configuration |
| RP-Initiated Logout 1.0 | Final / 2022-09-12 | end_session_endpoint |
| Front-Channel Logout 1.0 | Final / 2022-09-12 | iframe 経由のログアウト伝播 |
| Back-Channel Logout 1.0 | errata set 1 / 2023-12-15 | サーバー間のログアウト通知 |
| RFC 9700 | BCP 240 / 2025 | implicit の可否、nonce と PKCE の関係 |
| RFC 9101 | Standards Track | 認可リクエストを JWT で送る(JAR) |
Core 2.0 のドラフトは存在しない。 現行は errata set 2 が最新で、
Core 1.0 と書かれていても中身は 2023 年の版。
OIDC が OAuth に足したもの
Section titled “OIDC が OAuth に足したもの”フローは変わらない。増えるのは値と検証手順だけ。
flowchart TB
subgraph OAUTH["OAuth 2.0 (認可) — 変わらない部分"]
A1["認可コードフロー + PKCE"]
A2["access_token / refresh_token"]
A3["scope による権限の絞り込み"]
end
subgraph OIDC["OpenID Connect (認証) — 足される部分"]
B1["scope に openid を入れる"]
B2["id_token が返る (署名付き JWT)"]
B3["UserInfo エンドポイント"]
B4["nonce / auth_time / acr"]
end
OAUTH --> OIDC
OIDC --> USE["誰がログインしたかを<br/>クライアントが確認できる"]
style OIDC fill:#4054b2,color:#fff
style USE fill:#1f5c33,color:#fff
| access token | ID token | |
|---|---|---|
| 何を表すか | このクライアントは何をしてよいか | 誰がいつ認証されたか |
| 宛先 | リソースサーバー | クライアント自身(aud = client_id) |
| 形式 | 不透明でも JWT でもよい | 必ず署名付き JWT |
| 中身を読むのは | リソースサーバー | クライアント |
| API に送るか | 送る | 送らない |
| 何度も使うか | 有効期限まで使う | ログイン時に 1 回検証して捨てる |

id_token は身分証、access token は鍵。 身分証は自分が受け取って確認するもので、
どこかへ提出して回るものではない。鍵は相手に渡して使わせるもの。
id_token を API へ送るのは、身分証を鍵穴に差そうとしているのと同じ。
id_token は「使い続けるトークン」ではない。 受け取って検証して、
結果を自前のセッション(Cookie とセッション)に落としたら役目は終わる。
認可コードフローに openid scope と nonce を足しただけ。
sequenceDiagram
autonumber
participant UA as 利用者 (ブラウザ)
participant RP as RP (クライアント)
participant OP as OP (認可サーバー)
UA->>RP: ログインを開始する
RP->>RP: state / nonce / code_verifier<br/>を生成しセッションに保存
RP-->>UA: 302 認可エンドポイントへ
UA->>OP: GET /authorize<br/>scope=openid profile<br/>nonce / state / code_challenge
OP-->>UA: ログイン画面と同意画面
UA->>OP: 認証して scope に同意
OP-->>UA: 302 redirect_uri へ<br/>code / state / iss
UA->>RP: GET /callback
RP->>OP: POST /token<br/>code と code_verifier
OP-->>RP: access_token / id_token<br/>refresh_token
RP->>RP: id_token を 13 ステップで検証<br/>nonce を照合する
RP->>OP: GET /userinfo<br/>Authorization: Bearer ...
OP-->>RP: sub とクレーム
RP->>RP: UserInfo の sub が<br/>id_token の sub と一致するか
RP-->>UA: 自前のセッション Cookie を発行
11 番と 14 番がこのページの主題。 OAuth 2.0 のフローとの差はここにしかない。
利用者から見えるのは 5 番と 6 番だけで、実際にはこういう画面になる。

左が RP、右が OP の画面。 左のコーラルで消してある入力欄がパスワード欄で、 RP 側にはパスワード欄が存在しないことを表している。認証は右の OP 側で完結する。
右は prompt=select_account を送ったときに出るアカウント選択で、
複数アカウントを持つ利用者にどれで入るかを選ばせる
(後述)。
用語も OAuth と対応している。試験では OIDC の用語で問われる。
| OIDC の用語 | OAuth 2.0 の用語 |
|---|---|
| RP(Relying Party) | client |
| OP(OpenID Provider) | authorization server |
| End-User | resource owner |
| ID Token | (対応なし。OIDC が追加した) |
データフローを追う
Section titled “データフローを追う”実値で追う。iss は https://as.example.com、client_id は s6BhdRkqt3。
署名は HS256 にしてある(読者が手元で再現できるように)。
実際の OP は RS256 か ES256 を使う — 理由は後述。
1. scope に openid を入れる
Section titled “1. scope に openid を入れる”OIDC を有効にする唯一のスイッチ。Core §3.1.2.1 は
“Must contain the openid scope value” と定める。
GET /authorize?response_type=code &client_id=s6BhdRkqt3 &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=openid%20profile%20email &state=mkHXw-BfKLY &nonce=n-0S6_WzA2Mj &code_challenge=sOiV9ZgIX37UdtxEUzYQz7zKdaDeBwRb8ilMwpFVg4c &code_challenge_method=S256 HTTP/1.1Host: as.example.comopenid を落とすと id_token が返らず、ただの OAuth になる。
profile と email が増えているのは、返してほしいクレームの指定(後述)。
2. nonce の要否はフローで変わる
Section titled “2. nonce の要否はフローで変わる”ここが最も取り違えられる。 Core §3.1.2.1 の nonce の扱いは一様ではない。
| フロー | nonce の要否 |
|---|---|
認可コードフロー(response_type=code) |
OPTIONAL |
implicit フロー(response_type=id_token など) |
REQUIRED |
hybrid フロー(code id_token / code id_token token) |
REQUIRED |
「nonce は常に必須」は誤り。ただし認可コードフローでも入れる。
理由は 2 つ。
nonceを送ったら、id_tokenにnonceクレームが必ず含まれ、値の照合が MUST になる (Core §3.1.3.7 ステップ 11)。送らなければこの検証が発生しない- RFC 9700 §2.1.1 が、confidential な OIDC クライアントに限り PKCE の代わりに
nonceを使ってよい(MAY)としている。裏を返せばnonceには固有の防御価値がある
3. トークンレスポンスに id_token が増える
Section titled “3. トークンレスポンスに id_token が増える”{ "access_token": "2YotnFZFEjr1zCsicMWpAA", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "scope": "openid profile email", "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtMDkifQ.eyJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tIiwic3ViIjoiMjQ4Mjg5NzYxMDAxIiwiYXVkIjoiczZCaGRSa3F0MyIsImV4cCI6MTc4OTAwMzYwMCwiaWF0IjoxNzg5MDAwMDAwLCJhdXRoX3RpbWUiOjE3ODg5OTk5NzAsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiYXRfaGFzaCI6ImJKWVREeE1Lc05iUldEbC1KTks4d1EifQ.Wk7Wkvby3HWLX3QED44LAQWAWD4rYbTQXnxflt11QAY"}id_token は 353 文字。. で 3 つに割れるのが JWT
(詳細は Bearer トークン)。
4. ID トークンを分解する
Section titled “4. ID トークンを分解する”ヘッダ(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtMDkifQ、58 文字):
{ "alg": "HS256", "typ": "JWT", "kid": "2026-09" }ペイロード(250 文字):
{ "iss": "https://as.example.com", "sub": "248289761001", "aud": "s6BhdRkqt3", "exp": 1789003600, "iat": 1789000000, "auth_time": 1788999970, "nonce": "n-0S6_WzA2Mj", "at_hash": "bJYTDxMKsNbRWDl-JNK8wQ"}手元で確認できる(macOS の base64 は復号が -D):
ID_TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjYtMDkifQ.eyJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tIiwic3ViIjoiMjQ4Mjg5NzYxMDAxIiwiYXVkIjoiczZCaGRSa3F0MyIsImV4cCI6MTc4OTAwMzYwMCwiaWF0IjoxNzg5MDAwMDAwLCJhdXRoX3RpbWUiOjE3ODg5OTk5NzAsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiYXRfaGFzaCI6ImJKWVREeE1Lc05iUldEbC1KTks4d1EifQ.Wk7Wkvby3HWLX3QED44LAQWAWD4rYbTQXnxflt11QAY'
b64url_decode() { local s="$1"; s="${s//-/+}"; s="${s//_//}" while [ $(( ${#s} % 4 )) -ne 0 ]; do s="${s}=" ; done printf '%s' "$s" | base64 -D ; }
b64url_decode "$(echo "$ID_TOKEN" | cut -d. -f2)" | jq .5. クレームの要否
Section titled “5. クレームの要否”Core §2。REQUIRED は 5 つだけで、あとは条件付き。
| クレーム | 要否 | 意味と注意 |
|---|---|---|
iss |
REQUIRED | 発行者。完全一致で照合する |
sub |
REQUIRED | 利用者の識別子。“MUST NOT exceed 255 ASCII characters” |
aud |
REQUIRED | “MUST contain the OAuth 2.0 client_id”。配列になりうる |
exp |
REQUIRED | 有効期限。UTC の 1970 年からの秒数 |
iat |
REQUIRED | 発行時刻。古すぎるトークンを弾くのに使う |
auth_time |
OPTIONAL | 利用者が実際に認証された時刻。 max_age を送ったら実質必須 |
nonce |
OPTIONAL | 送ったなら必ず入り、照合が MUST になる |
acr |
OPTIONAL | 認証の強度(多要素かどうかなど) |
amr |
OPTIONAL | 認証手段の配列 |
azp |
OPTIONAL | authorized party。あれば client_id と一致するか SHOULD |
at_hash |
OPTIONAL | access token のハッシュ。認可コードフローでのみ意味を持つ |
6. at_hash はハッシュの左半分だけを使う
Section titled “6. at_hash はハッシュの左半分だけを使う”この「左半分」が試験で問われる。 Core §3.1.3.8:
Hash the octets of the ASCII representation of the
access_token… Take the left-most half of the hash and base64url-encode it.
ハッシュ関数は id_token の alg に連動する。HS256 / RS256 なら SHA-256、
HS512 なら SHA-512。
flowchart TB
AT["access_token<br/>2YotnFZFEjr1zCsicMWpAA"]
ALG["id_token の alg を見る<br/>HS256 / RS256 → SHA-256"]
H["SHA-256 を計算<br/>32 バイト (256 bit)"]
L["左 16 バイト (128 bit) だけ取る<br/>右半分は捨てる"]
B["base64url にする<br/>bJYTDxMKsNbRWDl-JNK8wQ"]
AT --> ALG --> H --> L --> B
style L fill:#7a4a12,color:#fff
style B fill:#4054b2,color:#fff
シェルで再現できる。head -c 16 が「左半分」そのもの:
b64url() { base64 | tr '+/' '-_' | tr -d '=' ; }
printf '%s' '2YotnFZFEjr1zCsicMWpAA' | openssl dgst -binary -sha256 | head -c 16 | b64url# bJYTDxMKsNbRWDl-JNK8wQ ← id_token の at_hash と一致する同じ計算を認可コードに対してやったものが c_hash(hybrid フローで使う):
printf '%s' 'SplxlOBeZQQYbYS6WxSbIA' | openssl dgst -binary -sha256 | head -c 16 | b64url# o1uBp9eSe3DsmScN0jYriA| クレーム | 対象 | 使う場面 |
|---|---|---|
at_hash |
access_token |
認可コードフロー(id_token に含まれるとき) |
c_hash |
認可コード code |
hybrid フローでは REQUIRED |
目的は「一緒に返ってきた値がすり替えられていないか」の確認。
フロントチャネルで id_token と access token が同時に返る hybrid フローでは、
この結び付けがないと別々に差し替えられる。
7. ID トークンの検証(13 ステップ)
Section titled “7. ID トークンの検証(13 ステップ)”Core §3.1.3.7 が定める。署名検証はこのうちの 1 つでしかない。
| # | 内容 | 強度 |
|---|---|---|
| 1 | 暗号化されていれば復号する | 条件付き |
| 2 | iss が OP の Issuer 識別子と完全一致するか |
MUST |
| 3 | aud に自分の client_id が含まれるか |
MUST |
| 4 | aud に自分以外が含まれるなら扱いを決めておく |
MUST |
| 5 | azp があれば client_id と一致するか |
SHOULD |
| 6 | トークンエンドポイントから直接受け取ったなら TLS の検証で代替してよい | MAY |
| 7 | alg が登録した id_token_signed_response_alg(既定 RS256)と一致するか |
SHOULD |
| 8 | MAC 系(HS256 など)なら client_secret の UTF-8 バイト列を鍵にする |
— |
| 9 | 現在時刻が exp より前であるか |
MUST |
| 10 | iat が古すぎないか(許容範囲は実装が決める) |
MAY |
| 11 | nonce を送ったなら、nonce クレームがあり値が一致するか |
MUST |
| 12 | acr を要求したなら妥当な値か |
SHOULD |
| 13 | max_age を送ったなら auth_time を見て再認証が必要か判断する |
SHOULD |
flowchart TB
IN["id_token を受け取る"]
S["署名を検証する<br/>jwks_uri の鍵 / kid で選ぶ"]
C2{"iss が完全一致するか"}
C3{"aud に自分の client_id が<br/>含まれるか"}
C9{"現在時刻 < exp か"}
C11{"送った nonce と<br/>nonce クレームが一致するか"}
OK["ログイン成立<br/>自前のセッションを発行"]
NG["拒否する<br/>ログインさせない"]
IN --> S
S -->|"失敗"| NG
S -->|"成功"| C2
C2 -->|"いいえ"| NG
C2 -->|"はい"| C3
C3 -->|"いいえ"| NG
C3 -->|"はい"| C9
C9 -->|"いいえ"| NG
C9 -->|"はい"| C11
C11 -->|"いいえ"| NG
C11 -->|"はい"| OK
style OK fill:#1f5c33,color:#fff
style NG fill:#7a2222,color:#fff
8. UserInfo の sub を照合する
Section titled “8. UserInfo の sub を照合する”Core §5.3。access token を Bearer として提示して呼ぶ。
curl -s https://as.example.com/userinfo \ -H 'Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA' | jq .{ "sub": "248289761001", "name": "山田 太郎", "preferred_username": "taro", "email": "taro@example.com", "email_verified": true}返ってきた sub が id_token の sub と一致することを確認しなければならない。
仕様の文言(Basic Client Implementer’s Guide §2.3.2、Core §5.3.2 と同内容):
Due to the possibility of token substitution attacks, the UserInfo Response is not guaranteed to be about the End-User identified by the
sub… ThesubClaim in the UserInfo Response MUST be verified to exactly match thesubClaim in the ID Token
理由は access token が Bearer トークンだから。
別人の access token を握らされたら、UserInfo はその別人の情報を返す。
id_token の sub と突き合わせて初めて「同じ人の話」だと確定する。
9. UserInfo は必須の経路ではない
Section titled “9. UserInfo は必須の経路ではない”id_token に直接クレームを入れられる。 UserInfo を呼ばないと利用者情報が取れない、
というのは誤り。
| 取り方 | いつ選ぶか |
|---|---|
id_token のクレームを読む |
1 往復で済む。 変わらない属性(sub、email)向け |
| UserInfo を呼ぶ | 属性が多い・変わりうる。id_token を小さく保ちたい |
OP によって「id_token にどこまで入れるか」の既定が違う。
両方に対応するコードを書くのが安全で、id_token に無ければ UserInfo を引く。
nonce と state と PKCE の役割分担
Section titled “nonce と state と PKCE の役割分担”3 つとも「返ってきた応答を信じてよいか」に関わるが、防ぐものが違う。
OAuth 2.0 のページで state / PKCE / iss を分けたが、
OIDC ではここに nonce が加わる。
flowchart TB
Q["コールバックで受けた値を<br/>信じてよいか"]
S["state<br/>セッションに保存した値"]
P["PKCE<br/>code_verifier"]
N["nonce<br/>id_token のクレーム"]
Q --> S
Q --> P
Q --> N
S --> SA["自分が始めたフローか<br/>(CSRF)"]
P --> PA["この code を交換しに来たのは<br/>認可要求を出した本人か"]
N --> NA["この id_token は<br/>今回のログインのものか<br/>(リプレイ / トークン注入)"]
style S fill:#4054b2,color:#fff
style P fill:#4054b2,color:#fff
style N fill:#1f5c33,color:#fff
| 仕組み | 生成する側 | 経路 | 照合する場所 |
|---|---|---|---|
state |
RP | 認可リクエスト → 認可レスポンス | RP のセッションと突き合わせる |
code_verifier |
RP | フロントチャネルに出ない | OP がハッシュを突き合わせる |
nonce |
RP | 認可リクエスト → id_token のクレーム |
RP のセッションと突き合わせる |
「PKCE があれば nonce は不要」は条件付きで誤り
Section titled “「PKCE があれば nonce は不要」は条件付きで誤り”RFC 9700 §2.1.1 の文言は限定的:
With additional precautions … confidential OpenID Connect clients MAY use the
nonceparameter and the respective Claim in the ID Token instead.
読み方は「PKCE が第一選択、confidential な OIDC クライアントに限り nonce で代替可」。
逆向きに「nonce があるから PKCE 不要」でも、
「PKCE があるから nonce 不要」でもない。防ぐ対象が違う。
| PKCE が防ぐもの | nonce が防ぐもの |
|
|---|---|---|
| 対象 | 認可コードの傍受・注入 | id_token のリプレイ・注入 |
| 検証する側 | OP(トークンエンドポイント) | RP(id_token を読むとき) |
| public クライアントで免除されるか | されない(MUST) | されない |
実務の結論: 両方入れる。 認可コードフローでも nonce を送る。
コストは乱数 1 個とセッションの 1 フィールドしかない。
sub の扱い
Section titled “sub の扱い”sub だけをユーザーの主キーにしてはいけない。 Core §2 の定義:
Subject Identifier. A locally unique and never reassigned identifier within the Issuer
within the Issuer — 一意なのは発行者の中だけ。「Google でログイン」と
「GitHub でログイン」の両方に対応すると、sub が衝突しうる。
flowchart TB
K["利用者を一意に決める<br/>主キーは何か"]
BAD["sub だけを使う"]
GOOD["iss + sub の組を使う"]
K --> BAD
K --> GOOD
BAD --> B1["別の OP の利用者と<br/>衝突しうる"]
GOOD --> G1["OP をまたいでも衝突しない"]
GOOD --> G2["同じ人が別の OP で入ったら<br/>別アカウントとして扱われる"]
style BAD fill:#7a2222,color:#fff
style GOOD fill:#1f5c33,color:#fff
email を主キーにするのはもっと悪い。 変わるし、email_verified が
false のこともあり、OP によっては使い回される。
public と pairwise
Section titled “public と pairwise”Core §8。同じ利用者に対して、sub を全 RP で同じにするか RP ごとに変えるか。
| 種別 | 挙動(§8.1) | 使う場面 |
|---|---|---|
public |
“The same sub value is returned to all clients” |
社内 SSO。RP 間で利用者を突き合わせたい |
pairwise |
“A different sub value is returned to each client” |
一般公開の IdP。 RP 間の名寄せを防ぐ |
pairwise が保証するのは
“the End-User’s PPID at another Relying Party cannot be correlated with the End-User’s
PPID at this Relying Party” — つまり複数の RP が結託しても同一人物だと分からない。
Discovery — well-known のパスが RFC 8414 と違う
Section titled “Discovery — well-known のパスが RFC 8414 と違う”ここは実際に踏む。 OIDC Discovery §4 と RFC 8414 §3.1 で
/.well-known/ を挿入する位置が違う。
Issuer が https://as.example.com/tenant1 のとき:
| 仕様 | 取得先 URL |
|---|---|
| OIDC Discovery | https://as.example.com/tenant1/.well-known/openid-configuration |
| RFC 8414 | https://as.example.com/.well-known/oauth-authorization-server/tenant1 |
OIDC は Issuer の後ろに連結する
(“the path formed by concatenating the string /.well-known/openid-configuration to the Issuer”)。
RFC 8414 は ホストとパスの間に挿入する
(“insert … between the host component and the path component”)。
Issuer にパスが付かない(https://as.example.com)なら結果は同じなので、
マルチテナントの OP を扱うまで気づかない。
curl -s https://as.example.com/.well-known/openid-configuration | jq .{ "issuer": "https://as.example.com", "authorization_endpoint": "https://as.example.com/authorize", "token_endpoint": "https://as.example.com/token", "userinfo_endpoint": "https://as.example.com/userinfo", "jwks_uri": "https://as.example.com/jwks.json", "end_session_endpoint": "https://as.example.com/logout", "response_types_supported": ["code", "code id_token"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["RS256", "ES256"], "code_challenge_methods_supported": ["S256"]}REQUIRED なのは issuer / authorization_endpoint / response_types_supported /
subject_types_supported / id_token_signing_alg_values_supported / jwks_uri。
userinfo_endpoint は RECOMMENDED で、REQUIRED ではない。
alg を信用しない
Section titled “alg を信用しない”検証ステップ 7 は「alg が登録した id_token_signed_response_alg(既定 RS256)と
一致するか」を SHOULD で求めている。トークンの中の alg を読んで、それに従って
検証してはいけない。
| やること | やってはいけないこと |
|---|---|
期待する alg を自分のコードに書く |
トークンの alg を読んで分岐する |
jwks_uri から公開鍵を取り、kid で選ぶ |
トークンの jku / x5u を取得する |
alg: none を無条件に拒否する |
ライブラリの既定に任せる |
これは Bearer トークンのページで扱った RFC 8725 の
アルゴリズム混同と同じ話。RS256 を期待している検証器に HS256 の
トークンを投げると、公開鍵が HMAC の共有鍵として使われてしまう
(公開鍵は誰でも入手できるので、攻撃者が任意のトークンを署名できる)。
このページの例で HS256 を使ったのは読者が手元で再現できるようにするためで、
実運用では RS256 か ES256 を使う。HS256 は client_secret を鍵に使う
(ステップ 8)ので、public クライアントでは成立しない。
scope とクレームの対応
Section titled “scope とクレームの対応”Core §5.4。scope を足すと返るクレームが増える。
| scope | 返るクレーム |
|---|---|
openid |
(必須。id_token が返るようになる) |
profile |
name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at |
email |
email email_verified |
address |
address |
phone |
phone_number phone_number_verified |
profile は 14 個まとめて要求する粗い scope。 名前だけ欲しいときも
生年月日や性別まで要求することになるので、同意画面が重くなる。
細かく切りたいなら claims パラメータで個別に要求する。
フローの選択
Section titled “フローの選択”Core は 3 つのフローを定めるが、新規に選ぶのは 1 つだけ。
| フロー | response_type |
トークンの返り方 | 現在の評価 |
|---|---|---|---|
| 認可コード | code |
すべてトークンエンドポイント経由 | これを使う |
| implicit | id_token / id_token token |
すべてフラグメントで返る | RFC 9700 §2.1.2 が SHOULD NOT |
| hybrid | code id_token など |
code + 一部がフラグメント |
条件付きで許容 |
RFC 9700 §2.1.2 が SHOULD NOT にしているのは
“the implicit grant (response type token) or other response types issuing access
tokens in the authorization response” — つまり
access token をフロントチャネルに載せる方式。
同じ節が代替として
“such as the code id_token response type” を挙げている。
code id_token は id_token はフラグメントで返るが access token は
トークンエンドポイント経由なので、この禁止に当たらない。
flowchart TB
Q1{"新規に作るか"}
Q2{"認可レスポンスで<br/>access_token を<br/>受け取るか"}
Q3{"id_token を先に<br/>受け取る必要があるか"}
CODE["認可コードフロー<br/>response_type=code<br/>+ PKCE + nonce"]
HYB["hybrid<br/>code id_token<br/>c_hash の検証が必要"]
IMP["implicit は使わない<br/>RFC 9700 SHOULD NOT"]
Q1 -->|"はい"| CODE
Q1 -->|"既存の移行"| Q2
Q2 -->|"はい"| IMP
Q2 -->|"いいえ"| Q3
Q3 -->|"はい"| HYB
Q3 -->|"いいえ"| CODE
style CODE fill:#1f5c33,color:#fff
style HYB fill:#7a4a12,color:#fff
style IMP fill:#7a2222,color:#fff
hybrid を選ぶなら c_hash の検証が増える。 認可コードフローで済むなら
そちらのほうが実装が単純で、検証項目も少ない。
OIDC のログアウトは 3 つの独立した仕様に分かれている。 混同しやすい。
flowchart TB
L["ログアウトさせたい"]
RPI["RP-Initiated Logout<br/>RP が OP へリダイレクト"]
FC["Front-Channel Logout<br/>OP が iframe を並べる"]
BC["Back-Channel Logout<br/>OP が RP へ直接 POST"]
L --> RPI
L --> FC
L --> BC
RPI --> R1["利用者を OP から<br/>ログアウトさせる"]
FC --> F1["他の RP にも伝える<br/>ブラウザ経由"]
BC --> B1["他の RP にも伝える<br/>サーバー間"]
F1 --> F2["3rd party Cookie が<br/>ブロックされると失敗する"]
style BC fill:#1f5c33,color:#fff
style F2 fill:#7a2222,color:#fff
RP-Initiated Logout
Section titled “RP-Initiated Logout”RP が利用者を end_session_endpoint へリダイレクトする。
GET /logout?id_token_hint=eyJhbGciOi... &post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Fbye &state=mkHXw-BfKLY HTTP/1.1Host: as.example.com| パラメータ | 要否 |
|---|---|
id_token_hint |
RECOMMENDED(誰をログアウトさせるかを OP に伝える) |
logout_hint |
OPTIONAL |
client_id |
OPTIONAL |
post_logout_redirect_uri |
OPTIONAL(事前登録が必要) |
state |
OPTIONAL |
end_session_endpoint はメタデータでは REQUIRED(仕様に対応する OP なら必ず公開する)。
Front-Channel と Back-Channel の違い
Section titled “Front-Channel と Back-Channel の違い”「他の RP にもログアウトを伝える」ための仕組みで、伝え方が違う。
| Front-Channel | Back-Channel | |
|---|---|---|
| 経路 | OP が各 RP の URI を iframe で並べたページを返す | OP が RP へ直接 POST |
| RP 側の登録 | frontchannel_logout_uri |
backchannel_logout_uri |
| 送るもの | クエリの iss / sid(任意) |
logout_token(JWT) |
| ブラウザ依存 | する | しない |
| 確実性 | 低い。3rd party Cookie がブロックされると RP のセッション Cookie に届かない | 高い |
| 制約 | なし | RP のエンドポイントが外部から到達可能である必要がある |
Front-Channel の限界は仕様自身が明記している。 サードパーティ Cookie の
ブロックが標準になった今、iframe から RP のセッション Cookie を消せる保証はない。
Cookie とセッションの SameSite の話と同じ流れ。
Back-Channel の logout_token は普通の id_token ではない。
eventsクレームの値はhttp://schemas.openid.net/event/backchannel-logout- “MUST contain either a
subor asidClaim, and MAY contain both” nonceクレームは禁止(id_tokenと取り違えて使われるのを防ぐため)
認可リクエストを JWT で送る(JAR)
Section titled “認可リクエストを JWT で送る(JAR)”RFC 9101。クエリパラメータは改ざんできる (“the communication through the user agents is not integrity protected”)。 これを JWT に載せて署名する。
| パラメータ | 渡し方 |
|---|---|
request |
認可リクエストのパラメータ一式を JWS で署名した JWT として値渡し |
request_uri |
その JWT を OP が取りに来る HTTPS URI(512 ASCII 文字以内) |
金融系(FAPI)で必須になる。 通常の Web アプリでは
state / nonce / PKCE で足りるが、「リクエスト自体の完全性」が要件になる場面で使う。
使ってよいケースと使ってはいけないケース
Section titled “使ってよいケースと使ってはいけないケース”「OIDC は安全か」ではなく「認証を外部に委ねてよいか」で判断する。
成立の前提条件
Section titled “成立の前提条件”- 利用者の認証を外部の OP に委ねてよい(自前でパスワードを持たない選択ができる)
- OP が Discovery(
/.well-known/openid-configuration)を公開している -
iss+subを主キーにしたアカウント設計になっている -
id_tokenを 13 ステップで検証する実装(またはライブラリ)がある -
jwks_uriから鍵を取得し、kidで選び、ローテーションに追従できる - 認証後は自前のセッションに落とす(
id_tokenを使い回さない) -
subject_types(public/pairwise)を最初に決めている
ひとつでも欠けたら止まる。 特に 3 つ目と 6 つ目が欠けたまま作ると、 後から直すのにデータ移行が必要になる。

試験前に見返すならこの 1 枚。 コーラルで示した iss + sub の複合キーは、
後から直すのに全ユーザーのデータ移行が必要になる唯一の項目。
使うかどうかの判定
Section titled “使うかどうかの判定”flowchart TB
Q1{"必要なのは<br/>認証か認可か"}
Q2{"認証を外部の OP に<br/>委ねてよいか"}
Q3{"OP は Discovery と<br/>jwks_uri を公開しているか"}
OK["OIDC + 認可コードフロー<br/>+ PKCE + nonce"]
NG1["OAuth 2.0 だけで足りる<br/>id_token は要らない"]
NG2["自前のログインを作る<br/>Cookie セッション"]
NG3["OP を選び直す<br/>手組みしない"]
Q1 -->|"認可だけ"| NG1
Q1 -->|"認証"| Q2
Q2 -->|"いいえ"| NG2
Q2 -->|"はい"| Q3
Q3 -->|"いいえ"| NG3
Q3 -->|"はい"| OK
style OK fill:#1f5c33,color:#fff
style NG1 fill:#7a2222,color:#fff
style NG2 fill:#7a2222,color:#fff
style NG3 fill:#7a2222,color:#fff
使ってよいケース
Section titled “使ってよいケース”| ケース | 成立する理由 | 一緒にやること |
|---|---|---|
| 「Google でログイン」「GitHub でログイン」 | パスワードを自前で持たなくて済む | iss + sub を主キーに。email は表示用 |
| 社内 SSO(複数の業務アプリ) | 認証を 1 か所に集約でき、退職時の失効も 1 か所 | subject_types は public。Back-Channel Logout |
| B2B SaaS のテナントごとの IdP 接続 | テナントが自社の IdP を使える | Discovery を動的に取得。Issuer ごとにアカウントを分ける |
| モバイルアプリのログイン | 外部ブラウザに認証を委ね、アプリはパスワードを見ない | PKCE 必須。RFC 8252 に従い WebView を使わない |
| 多要素認証の要求 | acr_values / max_age で強度と鮮度を要求できる |
acr と auth_time を実際に検証する |
使ってはいけないケース
Section titled “使ってはいけないケース”| ケース | できないこと |
|---|---|
id_token を API のアクセス制御に使う |
aud がクライアント宛なので、リソースサーバー向けの認可判断には使えない。access token を使う |
sub 単体を主キーにする |
OP をまたぐと衝突する。iss + sub にする |
email を主キーにする |
変わる。email_verified: false がありうる |
id_token をセッション代わりに使い回す |
exp が数分。毎リクエストで検証するのは無駄で、失効もできない |
| implicit フローを新規採用する | RFC 9700 §2.1.2 が SHOULD NOT |
aud を検証しない |
同じ OP の別クライアント向けトークンで通ってしまう |
alg をトークンから読んで分岐する |
アルゴリズム混同攻撃を通してしまう |
| OP を自前でフルスクラッチする | Core だけで膨大。Keycloak などを使う |
| 種別 | 例 |
|---|---|
| OP(SaaS) | Google、Microsoft Entra ID、Auth0、Okta、Amazon Cognito |
| OP(セルフホスト) | Keycloak、Ory Hydra、Authentik、Zitadel |
| RP 側ライブラリ | openid-client(Node.js)、authlib(Python)、Spring Security OAuth2 Client |
| 検証ツール | OpenID Foundation の Certification |
Discovery から鍵まで取る
Section titled “Discovery から鍵まで取る”ISS=https://as.example.comcurl -s "$ISS/.well-known/openid-configuration" > oidc.json
# issuer が取得に使った URL と一致することを確認する(Discovery §4.3)jq -r --arg iss "$ISS" 'if .issuer == $iss then "issuer OK" else "ISSUER MISMATCH" end' oidc.json
jq -r '.jwks_uri' oidc.json | xargs curl -s | jq '.keys[] | {kid, kty, alg, use}'Node.js で ID トークンを検証する
Section titled “Node.js で ID トークンを検証する”jose を使う。ライブラリに任せる部分と自分で書く部分を分ける。
import { createRemoteJWKSet, jwtVerify } from 'jose';
const ISSUER = 'https://as.example.com';const CLIENT_ID = 's6BhdRkqt3';
// jwks_uri は Discovery から取る。鍵はライブラリがキャッシュとローテーションを扱うconst jwks = createRemoteJWKSet(new URL(`${ISSUER}/jwks.json`));
export async function verifyIdToken(idToken, { nonce, maxAge }) { // ステップ 2 / 3 / 7 / 9 はここで検証される const { payload } = await jwtVerify(idToken, jwks, { issuer: ISSUER, // iss の完全一致 (MUST) audience: CLIENT_ID, // aud に client_id を含むか (MUST) algorithms: ['RS256'], // 期待する alg を固定する。トークンから読まない clockTolerance: 5, // 秒。時計のずれの許容 });
// ステップ 11: nonce はライブラリが見ないので自分で照合する (MUST) if (nonce && payload.nonce !== nonce) { throw new Error('nonce mismatch'); }
// ステップ 5: azp があれば client_id と一致するか (SHOULD) if (payload.azp && payload.azp !== CLIENT_ID) { throw new Error('azp mismatch'); }
// ステップ 13: max_age を送ったなら auth_time の鮮度を見る (SHOULD) if (maxAge != null) { if (typeof payload.auth_time !== 'number') throw new Error('auth_time missing'); if (Date.now() / 1000 - payload.auth_time > maxAge) throw new Error('re-authentication required'); }
return payload;}nonce を検証するライブラリは少ない。 jose も jwtVerify では見ない。
「ライブラリを通したから大丈夫」で最も落ちやすいのがここ。
at_hash を検証する
Section titled “at_hash を検証する”import crypto from 'node:crypto';
// alg が *256 なら SHA-256、*384 なら SHA-384、*512 なら SHA-512function expectedHash(value, alg) { const sha = `sha${alg.slice(-3)}`; // RS256 -> sha256 const digest = crypto.createHash(sha).update(value, 'ascii').digest(); return digest.subarray(0, digest.length / 2).toString('base64url'); // 左半分だけ}
export function verifyAtHash(payload, accessToken, alg) { if (!payload.at_hash) return; // OPTIONAL なので無ければ飛ばす if (payload.at_hash !== expectedHash(accessToken, alg)) { throw new Error('at_hash mismatch'); }}検証後は自前のセッションに落とす
Section titled “検証後は自前のセッションに落とす”app.get('/callback', async (req, res) => { // state / PKCE の検証は OAuth 2.0 のページと同じ const tokens = await exchangeCode(req.query.code, req.session.codeVerifier);
const claims = await verifyIdToken(tokens.id_token, { nonce: req.session.nonce }); verifyAtHash(claims, tokens.access_token, 'RS256');
// 使い終わった一時値は捨てる delete req.session.nonce; delete req.session.codeVerifier; delete req.session.state;
// iss + sub の複合キーでユーザーを引く。sub 単体では引かない const user = await findOrCreateUser({ iss: claims.iss, sub: claims.sub });
// ここから先は自前のセッション。id_token は保存しない req.session.userId = user.id; res.redirect('/');});id_token はここで捨てる。 保存するのは
RP-Initiated Logout の id_token_hint に使う場合だけで、その場合も
「ログアウトのため」と用途を限定する。
運用で押さえる点
Section titled “運用で押さえる点”鍵のローテーションに追従する
Section titled “鍵のローテーションに追従する”jwks_uri の鍵は入れ替わる。kid でヘッダの鍵を選び、見つからなければ
JWKS を再取得するのが正しい挙動。
| やること | 理由 |
|---|---|
jwks_uri の応答をキャッシュする |
毎回取ると OP に負荷がかかる |
kid が未知なら再取得を 1 回だけ試す |
ローテーション直後に対応できる |
| 再取得にレート制限をかける | 未知の kid を大量に投げられると DoS になる |
jku / x5u は見ない |
トークンが鍵の取得先を指定できてしまう |
createRemoteJWKSet はこの挙動を実装している。自前で書くなら 4 つ全部要る。
exp / iat / auth_time はすべて秒単位の絶対時刻。
サーバーの時計がずれていると正しいトークンを弾く。
- NTP を入れる
clockToleranceは 数秒にする。分単位にするとexpの意味が薄れるiatが未来のトークンは拒否する
ログに残してはいけない値
Section titled “ログに残してはいけない値”| 値 | 扱い |
|---|---|
id_token |
残さない。 個人情報(email、name)が平文で入っている |
access_token / refresh_token |
残さない |
nonce / state |
残ってよい |
sub |
仮名だが個人の識別子。 扱いは利用者 ID と同じにする |
id_token は Base64 で読めるだけなので「暗号化されている」ように見える。
Basic 認証のページの「Base64 は暗号化ではない」がそのまま効く。
prompt と max_age で認証をやり直させる
Section titled “prompt と max_age で認証をやり直させる”| パラメータ | 効果 |
|---|---|
prompt=none |
画面を出さない。 すでにログイン済みかを確認するだけ(失敗なら login_required) |
prompt=login |
強制的に再認証させる |
prompt=consent |
同意画面を再表示する |
prompt=select_account |
アカウント選択画面を出す |
max_age=300 |
5 分以内に認証していなければ再認証させる。 auth_time で確認する |
prompt=none はサイレント更新に使う。iframe で叩いてセッションの生存を確認するが、
サードパーティ Cookie のブロックで動かなくなったので、
今は refresh token のローテーションで代替するのが実務。
引っかかりやすいポイント
Section titled “引っかかりやすいポイント”- OIDC は RFC ではない。OpenID Foundation の仕様で、現行は Core 1.0 errata set 2
-
scopeにopenidが無ければid_tokenは返らない -
nonceは認可コードフローでは OPTIONAL、implicit / hybrid では REQUIRED - それでも認可コードフローで
nonceを送る。送って初めて照合が MUST になる - 「PKCE があれば
nonce不要」は誤り。代替が許されるのは confidential クライアントのみ -
id_tokenのaudはclient_id。API へ送るトークンではない -
audの検証は署名検証とは別の MUST。ライブラリにaudienceを渡さないと見ない -
nonceの照合はライブラリがやらないことが多い。自分で書く -
algは登録した値と一致するかを見る。トークンのalgに従って分岐しない -
at_hash/c_hashは ハッシュの左半分だけを base64url にしたもの -
subは Issuer 内でのみ一意。主キーはiss+sub -
subは 255 ASCII 文字以内 -
emailを主キーにしない。email_verifiedを見ずに紐付けない -
expはトークンの寿命、auth_timeは利用者が認証した時刻。別物 - Discovery のパスは Issuer の後ろに連結。RFC 8414 はホストとパスの間に挿入
- 取得した設定の
issuerが取得 URL と一致するか を検証する -
userinfo_endpointはメタデータで RECOMMENDED(REQUIRED ではない) - UserInfo の
subがid_tokenのsubと一致するか検証する(MUST) - Front-Channel Logout はサードパーティ Cookie のブロックで失敗する。新規なら Back-Channel
-
logout_tokenはsubかsidのどちらかを必ず持つ。nonceは禁止 - 検証が済んだら自前のセッションに落とす。
id_tokenを使い回さない
OpenID Connect と OAuth 2.0 の関係を 1 行で。
OIDC は OAuth 2.0 の上に載る認証レイヤ。 Core §1 が “a simple identity layer on top of the OAuth 2.0 protocol” と説明する。
フローは OAuth の認可コードフローをそのまま使い、scope=openid を足すと
id_token が返るようになる。別物ではなく拡張。
OIDC を有効にする唯一のスイッチは何か。
scope に openid を含めること。 Core §3.1.2.1 が
“Must contain the openid scope value”。
これが無ければ id_token は返らず、ただの OAuth 2.0 のフローになる。
ID トークンの REQUIRED なクレームを 5 つ挙げよ。
iss / sub / aud / exp / iat(Core §2)。
auth_time / nonce / acr / amr / azp / at_hash は OPTIONAL だが、
条件次第で実質必須になる(max_age を送れば auth_time、
nonce を送れば nonce)。
`nonce` は必須か。フローごとに答えよ。
- 認可コードフロー(
response_type=code): OPTIONAL - implicit フロー: REQUIRED
- hybrid フロー(
code id_token/code id_token token): REQUIRED
「常に必須」は誤り。ただし認可コードフローでも送る — 送って初めて
id_token の nonce 照合が MUST(Core §3.1.3.7 ステップ 11)になるため。
PKCE があれば `nonce` は不要か。
不要にはならない。 RFC 9700 §2.1.1 が代替を認めているのは “confidential OpenID Connect clients” だけで、しかも “With additional precautions” という条件が付く。
そもそも防ぐ対象が違う。PKCE は認可コードの傍受・注入、nonce は
id_token のリプレイ・注入。 検証する側も違う(PKCE は OP、nonce は RP)。
`id_token` を API のアクセス制御に使ってよいか。
使ってはいけない。 aud は “the OAuth 2.0 client_id”(Core §2)で、
宛先がクライアント自身。リソースサーバー向けの認可トークンとして設計されていない。
API に送るのは access token。id_token は「誰がログインしたか」を
クライアントが確認するための値で、検証したら捨てる。
ID トークンの検証で、署名検証以外に MUST とされている項目を 3 つ挙げよ。
Core §3.1.3.7 より:
issが OP の Issuer 識別子と完全一致する(ステップ 2)audに自分のclient_idが含まれる(ステップ 3)- 現在時刻が
expより前(ステップ 9)
nonce を送った場合はその照合(ステップ 11)も MUST。
署名検証は 13 ステップのうちの 1 つに過ぎない。
`at_hash` はどう計算するか。
access_token の ASCII 表現を、id_token の alg に対応するハッシュ関数で
ハッシュし、左半分だけを base64url する(Core §3.1.3.8)。
RS256 / HS256 なら SHA-256(32 バイト)の左 16 バイト。
printf '%s' '2YotnFZFEjr1zCsicMWpAA' | openssl dgst -binary -sha256 | head -c 16 \ | base64 | tr '+/' '-_' | tr -d '='# bJYTDxMKsNbRWDl-JNK8wQ同じ計算を code に対してやったものが c_hash(hybrid フローで REQUIRED)。
利用者を一意に識別する主キーは何にすべきか。
iss + sub の組。 Core §2 の sub の定義は
“A locally unique and never reassigned identifier within the Issuer” —
一意なのは発行者の中だけ。
複数の OP に対応すると sub は衝突しうる。email はもっと悪い
(変わる、email_verified: false がありうる)。
`public` と `pairwise` の subject type の違いは。
public— “The samesubvalue is returned to all clients”(§8.1)pairwise— “A differentsubvalue is returned to each client”
pairwise は複数の RP が結託しても同一人物だと突き合わせられないことを保証する。
一般公開の IdP は pairwise、社内 SSO は public を選ぶ。
後から変えると全員の sub が変わるので、最初に決める。
UserInfo の応答で必ず検証すべきことは何か。
sub が id_token の sub と完全一致すること。
一致しなければ応答を使ってはならない(MUST)。
理由は access token が Bearer トークンだから。 別人の access token を握らされると、UserInfo はその別人の情報を返す (token substitution attack)。
OIDC の discovery パスは RFC 8414 と同じか。
違う。挿入位置が逆。 Issuer が https://as.example.com/tenant1 のとき:
- OIDC Discovery —
https://as.example.com/tenant1/.well-known/openid-configuration(後ろに連結) - RFC 8414 —
https://as.example.com/.well-known/oauth-authorization-server/tenant1(ホストとパスの間)
Issuer にパスが無ければ結果は同じなので、マルチテナントを扱うまで気づかない。
Discovery で取得した設定について、必ず検証すべきことは。
issuer の値が、取得に使った URL の prefix と完全一致すること(Discovery §4.3)。
これを飛ばすと、攻撃者のエンドポイントが "issuer": "https://as.example.com" と
名乗るだけで正規の OP として登録されてしまう。
Front-Channel Logout と Back-Channel Logout の違いと、新規に選ぶべき方を答えよ。
- Front-Channel — OP が各 RP の
frontchannel_logout_uriを iframe で並べたページを返し、ブラウザ経由で通知する - Back-Channel — OP が RP の
backchannel_logout_uriへ 直接 POST し、logout_token(JWT)を渡す
新規なら Back-Channel。 Front-Channel はサードパーティ Cookie が ブロックされると RP のセッション Cookie に届かず、失敗したことも分からない (この限界は仕様自身が明記している)。
`logout_token` が普通の `id_token` と違う点を挙げよ。
eventsクレームを持ち、値はhttp://schemas.openid.net/event/backchannel-logoutsubかsidのどちらかを必ず持つ(両方でもよい)nonceクレームは禁止 —id_tokenと取り違えて認証に使われるのを防ぐため
`exp` と `auth_time` の違いを説明せよ。
exp— ID トークン自体の有効期限。数分。過ぎたら捨てるだけauth_time— 利用者が実際に認証された時刻。セッションの鮮度を測る
「30 分以上前のログインなら再認証させたい」は auth_time で判断する
(max_age=1800 を送って要求する)。混同すると毎分ログインし直させる実装になる。
`id_token` の `alg` はどう扱うべきか。
期待する alg を自分のコードに固定し、トークンの alg に従って分岐しない。
Core §3.1.3.7 ステップ 7 が、登録した id_token_signed_response_alg
(既定 RS256)と一致するかを見るよう定めている。
トークンの alg を信じると、RS256 を期待する検証器に HS256 の
トークンを投げられ、公開鍵が HMAC の共有鍵として使われる
(アルゴリズム混同 — Bearer トークン参照)。
- OpenID Connect Core 1.0 (errata set 2)
- OpenID Connect Discovery 1.0 (errata set 2)
- OpenID Connect RP-Initiated Logout 1.0
- OpenID Connect Front-Channel Logout 1.0
- OpenID Connect Back-Channel Logout 1.0
- RFC 9700 — Best Current Practice for OAuth 2.0 Security
- RFC 9101 — JWT-Secured Authorization Request (JAR)
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 8725 — JSON Web Token Best Current Practices
- OpenID Certification