OpenID Connect(ID Token 與 UserInfo)
Signet 在 Authorization Code Flow 上支援 OpenID Connect 1.0。當您在 scope 中包含 openid,Signet 會在發出 access token 的同時簽發一張 ID token,並開放 /oauth/userinfo 端點。
目前 Device Flow 不簽發 ID token。要做 OIDC 請用 Authorization Code Flow。
ID Token vs. Access Token
| 問題 | ID Token | Access Token |
|---|---|---|
| 它在描述 _誰_? | 終端使用者(身分) | 呼叫 API 的授權 |
| 它 _給誰用_? | 您的客戶端應用(aud=client_id) |
它綁定的 resource server(aud=每次請求的 resource 或 JWT_AUDIENCE) |
能當 Authorization: Bearer 送 API? |
不行 — 永遠不行 | 可以 |
需要驗 aud? |
要 — 必須等於您的 client_id |
要 — 必須等於 resource server 的識別字(見 JWT 驗證 §Audience Binding) |
需要驗 nonce? |
要 — 必須與您送出的一致 | 不適用 |
| 包含個資? | 有(email、name、picture,視 scope 而定) | 無 |
原則:只有您自己的客戶端應用才應該去解析 ID token。把它傳給另一個服務,等於把使用者身分洩露給非預期對象。
索取 ID token
在 Authorization Code Flow 的授權請求中把 openid 放進 scope,並帶上 nonce:
GET /oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.example/callback
&response_type=code
&scope=openid profile email
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
在 /oauth/token 換取 token 的回應中會附上 id_token:
{
"access_token": "eyJhbG...",
"refresh_token": "def502...",
"id_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}
ID Token 的 claims
Header:
{
"alg": "RS256",
"kid": "abc123...",
"typ": "JWT"
}
Payload(依授予的 scope 而定):
| Claim | 必出現 | 出現時機 | 意義 |
|---|---|---|---|
iss |
✓ | Issuer URL — 必須等於探索文件中的 issuer |
|
sub |
✓ | 穩定的使用者識別(UUID) | |
aud |
✓ | 您的 client_id — 必須相符 才算有效 |
|
exp |
✓ | 過期時間(Unix 秒) | |
iat |
✓ | 簽發時間(Unix 秒) | |
auth_time |
✓ | 使用者認證時間(Unix 秒) | |
jti |
✓ | 唯一 token id | |
nonce |
— | 您在授權請求有帶 nonce 時 |
必須與您送出的值一致 — 防重送 |
at_hash |
— | 同時簽發 access token 時 | 對 access token 做 SHA-256 的前半段,base64url 編碼 |
name |
— | scope 含 profile |
顯示用全名 |
preferred_username |
— | scope 含 profile |
顯示用帳號(例如 alice) |
picture |
— | scope 含 profile 且使用者有大頭貼 |
頭像 URL |
updated_at |
— | scope 含 profile |
profile 最近更新時間(Unix 秒) |
email |
— | scope 含 email |
主要 email |
email_verified |
— | scope 含 email |
true 表示 email 已被驗證(例如透過 OAuth provider) |
驗證 ID token
與 access token 同樣使用 JWKS(見 JWT 驗證),但驗證規則 更嚴格:
- 簽章 — 以 JWKS 中
kid對應的金鑰驗證。
iss— 必須等於探索文件中的issuer(即BASE_URL的正規化形式:scheme 與主機小寫、去除結尾斜線)。
aud— 必須等於您的client_id。若aud是陣列,必須包含您的client_id且不得包含可疑值。
exp— 必須在未來(可容許少量時鐘偏移,例如 30 秒)。
iat— 應該是近期時間。
nonce— 必須等於您在授權請求送出的nonce。
auth_time— 若您帶了max_age,需強制檢查。
at_hash(選用、建議) — 驗證與同時拿到的 access token 相符。
Go(golang-jwt + keyfunc)
import (
"strings"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
jwksURL := "https://your-signet/.well-known/jwks.json"
k, _ := keyfunc.NewDefault([]string{jwksURL})
token, err := jwt.Parse(idTokenString, k.Keyfunc,
jwt.WithIssuer("https://your-signet"),
jwt.WithAudience(clientID), // 強制驗 aud
jwt.WithExpirationRequired(),
jwt.WithValidMethods([]string{"RS256", "ES256"}),
)
if err != nil {
return fmt.Errorf("invalid id_token: %w", err)
}
claims := token.Claims.(jwt.MapClaims)
nonce, ok := claims["nonce"].(string)
if !ok || nonce != expectedNonce {
return fmt.Errorf("nonce mismatch")
}
Python(PyJWT)
import jwt
from jwt import PyJWKClient
jwks_client = PyJWKClient(f"{SIGNET_URL}/.well-known/jwks.json")
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
claims = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256", "ES256"],
issuer=SIGNET_URL,
audience=CLIENT_ID, # 強制驗 aud
options={"require": ["exp", "iss", "sub", "aud"]},
)
if claims.get("nonce") != expected_nonce:
raise ValueError("nonce mismatch")
Node.js(jose)
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL(`${SIGNET_URL}/.well-known/jwks.json`));
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: SIGNET_URL,
audience: CLIENT_ID, // 強制驗 aud
algorithms: ["RS256", "ES256"],
});
if (payload.nonce !== expectedNonce) throw new Error("nonce mismatch");
UserInfo 端點
要取得 scope 授權的即時使用者資料,請用 access token(不是 ID token)呼叫 /oauth/userinfo:
curl -H "Authorization: Bearer ACCESS_TOKEN" https://your-signet/oauth/userinfo
回應(欄位取決於授予的 scope):
{
"sub": "user-uuid",
"iss": "https://your-signet",
"name": "Alice Example",
"preferred_username": "alice",
"picture": "https://...",
"updated_at": 1700000000,
"email": "alice@example.com",
"email_verified": true
}
- 一定包含
sub與iss
profilescope 控制name、preferred_username、picture、updated_at
emailscope 控制email、email_verified
若 token 無效或過期,UserInfo 回應 401 Unauthorized 並帶 WWW-Authenticate: Bearer error="invalid_token", resource_metadata="<issuer>/.well-known/oauth-protected-resource"。除非設定 PROTECTED_RESOURCE_METADATA_ENABLED=false,否則會帶上 resource_metadata 指標(RFC 9728 §5.1);客戶端可循此指標找到授權伺服器。
ID token 的 claim vs. UserInfo,該選哪個? ID token 是登入當下一次性的身分證明。若需要即時 profile 資料(例如使用者剛換頭像),用當下的 access token 打 UserInfo。
Discovery
OIDC 函式庫應該自動從這裡取得設定:
https://your-signet/.well-known/openid-configuration
完整文件格式見 開始使用。
Signet 也在
/.well-known/oauth-authorization-server發布平行的 RFC 8414 文件,提供給非 OIDC 的 OAuth 2.1 / MCP 客戶端。兩份文件共享的欄位內容保持一致 — 依您函式庫的需求挑一份用即可。當管理者啟用 Client ID Metadata Documents(CIMD_ENABLED=true)時,RFC 8414 文件會額外宣告client_id_metadata_document_supported: true— 其作用請見授權碼流程指南。
常見陷阱
- 把 ID token 當 Bearer 送去打 API。不要。用 access token。
- 不驗
aud。缺這一步,別人家客戶端的 ID token 可能被您誤接受。
- 不驗
nonce。一律送並驗nonce。規範雖然在 Auth Code Flow 標為 OPTIONAL,但省略就等於放棄防重送保護,強烈不建議。
- 沒驗簽就解析 ID token。千萬別這樣做 — 沒驗簽前的 JWT 是 未認證 的。
- 對 access token 驗
aud=client_id。Access token 的aud是 resource server 的識別字(RFC 8707resource或JWT_AUDIENCE),不是您的client_id。aud=client_id的驗法只適用於 ID token。