OIDC / OAuth 2.0#

Zeroword は Environment ごとに Canonical Issuer を持つ。アプリケーションは discovery から endpoint と署名鍵を取得し、Authorization Code + PKCE で認証する。 SPA は Browser SDK (@auth-platform/browser)、BFF は Server SDK (@auth-platform/server)、React Native は React Native SDK (@auth-platform/react-native) の案内も参照する。Hosted Login の画面挙動は Hosted Login、headless 認証は Public Authentication API、Client 登録は Management API、環境モデルは 概念とテナントモデル を参照する。

Issuer と discovery#

Issuer は必ず Environment ID を含む。root の /.well-known/openid-configuration ではなく、次の環境ごとの URL を使う。

export AUTH_ISSUER="https://id.zeroword.smartcrab.ai/e/<environmentId>"
curl -sS "$AUTH_ISSUER/.well-known/openid-configuration"

Discovery の issuer は AUTH_ISSUER と完全一致する。Discovery が返す全 endpoint もこの issuer の下にある。

Metadata field Endpoint
authorization_endpoint GET /oauth2/authorize
token_endpoint POST /oauth2/token
userinfo_endpoint GET /oauth2/userinfo
jwks_uri GET /oauth2/jwks
revocation_endpoint POST /oauth2/revoke
introspection_endpoint POST /oauth2/introspect
end_session_endpoint GET または POST /oauth2/logout

Discovery が返す主な capability は次のとおりである。

Field Values
response_types_supported code
grant_types_supported authorization_code, refresh_token
code_challenge_methods_supported S256
token_endpoint_auth_methods_supported client_secret_basic, client_secret_post, none
scopes_supported openid, profile, email, offline_access
subject_types_supported public
id_token_signing_alg_values_supported ES256
claims_supported iss, sub, aud, exp, iat, jti, sid, client_id, scope, amr, acr, auth_time, sv, nonce, name, email, email_verified, locale, updated_at

GET $AUTH_ISSUER/oauth2/jwks は {"keys":[...]} を返す。各 ES256 公開鍵は kty: EC, crv: P-256, x, y, kid, alg: ES256, use: sig を持つ。JWKS は Cache-Control: public, max-age=60 でキャッシュできる。署名鍵は Environment ごとに異なるため、他 Environment の issuer/key を流用しない。

Endpoint 一覧#

全 endpoint は issuer の下にあり、<environmentId> を含む。AUTH_ISSUER を discovery の issuer と一致させる。

Method Path suffix 用途 / 認証
GET /.well-known/openid-configuration 環境別 discovery。認証不要
GET /oauth2/authorize Authorization Code フロー開始。ブラウザーを Hosted Login へ redirect
POST /oauth2/token Authorization Code と Refresh Token grant
GET /oauth2/userinfo Authorization: Bearer <access_token> が必要
GET /oauth2/jwks JWT 検証用公開鍵。認証不要
POST /oauth2/introspect web_bff の client secret、または introspect Scope を持つ Management API key (X-Management-API-Key) が必要
POST /oauth2/revoke refresh token の family 失効。client authentication は要求されない
GET, POST /oauth2/logout RP-Initiated Logout。logout 用 redirect URI を検証して redirect

POST endpoint は application/x-www-form-urlencoded を受け付ける。JSON も受け付ける endpoint ではあるが、OAuth token/introspection/revocation の例では form encoding を使う。resource は form body で同じ名前を複数回送信できる。

Client 認証方式#

Client 作成時の token_endpoint_auth_method に設定する。Public client に secret は発行されない。

Client type Token endpoint authentication
web_spa none。client_id のみを送信し、PKCE を必ず使う
react_native none。client_id のみを送信し、PKCE を必ず使う
web_bff client_secret_basic または client_secret_post。secret と PKCE の両方を使う

client_secret_basic は HTTP Basic、client_secret_post は form body の client_id と client_secret で送る。どちらも token endpoint で利用できる。Client secret はサーバー側だけに保存し、SPA やアプリバイナリへ埋め込まない。

Authorization Code + PKCE#

対応する interactive flow は Authorization Code Grant のみである。BFF、SPA、React Native のすべてで PKCE S256 が必須。plain、Implicit、Resource Owner Password Credentials、Device Authorization、Dynamic Client Registration はサポートしない。

GET /oauth2/authorize の query parameter:

Parameter 要件
response_type 必須。code のみ
client_id 必須。active client ID
redirect_uri 必須。Client に登録した URI と byte-for-byte 一致
scope 必須。space-delimited。Client に許可された scope のみ
state 必須。1–512 文字。callback で client 側が元の値と照合する
code_challenge 必須。SHA-256 challenge の base64url、43 文字
code_challenge_method 必須。S256 のみ
nonce openid scope を要求する場合に必須。1–512 文字。ID token の nonce と照合する
resource 任意。Client 登録済み HTTPS API audience を最大 20 個指定できる。同じ resource parameter を繰り返す

prompt、max_age、login_hint は認証ポリシーに使われず無視される。未対応の response_type / plain、未知の scope token、query parameter の形式不正・必須値欠落は invalid_request になる。構文上有効でも openid に nonce がない場合や、Client status / 許可 scope / 登録 redirect URI / resource の業務検査に失敗した場合は Hosted Login の Interaction Domain error page へ redirect され、Client callback には code を返さない。

例では、SPA client の verifier/challenge を RFC 7636 に従って事前生成する。Verifier は 43–128 個の unreserved characters、challenge は BASE64URL(SHA256(verifier)) とする。

curl -iG "$AUTH_ISSUER/oauth2/authorize" \
  --data-urlencode 'response_type=code' \
  --data-urlencode "client_id=$AUTH_CLIENT_ID" \
  --data-urlencode "redirect_uri=$AUTH_REDIRECT_URI" \
  --data-urlencode 'scope=openid profile email offline_access' \
  --data-urlencode "state=$STATE" \
  --data-urlencode "nonce=$NONCE" \
  --data-urlencode "code_challenge=$CODE_CHALLENGE" \
  --data-urlencode 'code_challenge_method=S256'

Browser は Hosted Login へ redirect される。認証後に返された one-time code と同じ redirect_uri、保存していた code_verifier で token exchange する。Verifier は認証開始前に生成して callback まで保持する。Authorization code の有効期限は 60 秒。

curl -sS "$AUTH_ISSUER/oauth2/token" \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=$AUTH_CLIENT_ID" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=$AUTH_REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

web_bff は上の client_id を body から外し、次のいずれかで認証する。

# client_secret_basic
curl -sS "$AUTH_ISSUER/oauth2/token" \
  -u "$AUTH_CLIENT_ID:$AUTH_CLIENT_SECRET" \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=$AUTH_REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

# client_secret_post
curl -sS "$AUTH_ISSUER/oauth2/token" \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=$AUTH_CLIENT_ID" \
  --data-urlencode "client_secret=$AUTH_CLIENT_SECRET" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "redirect_uri=$AUTH_REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

成功時は JSON で access_token, token_type: "Bearer", expires_in, scope を返す。openid が付与された場合は id_token、offline_access が付与された場合は refresh_token も返る。

Token endpoint request は grant_type で次の形に分かれる。resource は任意で、複数値は form body の同名 field を繰り返す。JSON body を使う場合は array とする。

grant_type Required fields Optional fields
authorization_code code, redirect_uri, code_verifier resource
refresh_token refresh_token scope, resource

Public client は client_id を body に加える。BFF は client_secret_basic なら Basic header を、client_secret_post なら body の client_id, client_secret を使う。

Scope、Refresh Token、TTL#

Scope 効果
openid ID token を発行し、UserInfo を利用可能にする。authorize request では nonce も必須
profile ID token / UserInfo の name, locale, updated_at を許可する
email ID token / UserInfo の email, email_verified を許可する
offline_access Authorization Code exchange 時に Refresh Token を発行する

未知の scope、Client に未許可の scope は拒否される。email は Access Token に含まれない。

Value Lifetime
Authorization code 60 秒
Access token 300 秒(token response の expires_in: 300)
ID token 300 秒
Refresh token idle expiry 最終利用から 30 日
Refresh token absolute expiry 発行から 90 日
Login session idle expiry 7 日
Login session absolute expiry 30 日

Refresh Token は rt_<family-id>.<generation>.<secret> 形式の bearer credential で、毎回の成功した refresh で世代を進め、新しい refresh token を返す。旧 token を安全に破棄し、保存値を新しい token で置換する。scope と resource は元の grant の部分集合にだけ狭められる。省略した値は元の grant を維持する。

curl -sS "$AUTH_ISSUER/oauth2/token" \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode "client_id=$AUTH_CLIENT_ID" \
  --data-urlencode "refresh_token=$AUTH_REFRESH_TOKEN" \
  --data-urlencode 'scope=openid profile' \
  --data-urlencode 'resource=https://api.customer.example'

web_bff は同じ Basic または Post client authentication を使う。直前世代の concurrent retry 用に 5 秒の reuse window がある。その期間の同一 token の再送には勝者が生成した token response を返す。5 秒を過ぎて旧 generation を再使用すると、Refresh Token family 全体が失効し、現行 token も invalid_grant になる。盗難・再送時は最新 token だけを保有する。

JWT header、claims、audience#

Access Token と ID Token は Environment の署名鍵で ES256 署名され、header に kid を含む。JWT header の alg を信頼せず、ES256 固定で検証する。

Token Header typ 主な claims
Access Token at+jwt iss, sub, aud, exp, iat, jti, sid, client_id, scope, amr, acr, auth_time, sv
ID Token JWT iss, sub, aud, exp, iat, sid, auth_time, amr, acr, nonce。profile / email scope に応じ name, locale, updated_at, email, email_verified

主な claim の意味は次のとおりである。

Claim 意味
iss Environment の Canonical Issuer。Discovery の issuer と完全一致
sub Environment 内の user ID。ID/Access Token で同じ値
aud Access Token は要求した API resource、ID Token は client ID。単一 audience は string、複数は array
client_id Access Token を発行した client ID。ID Token では aud が client ID を示す
sid Login session ID。ID/Access Token の同じ session を結び付ける
scope Access Token に付与された space-delimited scope
amr 認証方法。email_otp、passkey、social。Refresh Token で再発行した token は refresh_token
acr 認証保証レベル。Passkey は urn:example-auth:loa:phishing-resistant、Email OTP / social login / Refresh Token は urn:example-auth:loa:basic
auth_time 元のユーザー認証時刻(Unix seconds)。Refresh で再発行時刻に更新しない
sv User security version。security version 更新後の token は UserInfo / introspection で inactive になる
jti Access Token ごとの JWT ID
iat JWT 発行時刻(Unix seconds)
exp JWT 失効時刻(Unix seconds)。exp - iat は access / ID token で 300 秒
nonce authorize request の nonce。ID Token に echo される
name, locale, updated_at profile scope がある場合だけ ID Token / UserInfo に含まれる。updated_at は Unix seconds
email, email_verified email scope がある場合だけ ID Token / UserInfo に含まれる

azp は発行 token に含まれず、claims_supported にもない。Access Token を API で利用する場合は署名だけで許可せず、期待する iss と API 固有 aud、期限を検証する。openid を要求していない token には ID Token がない。

Resource / audience binding#

API resource は Client の allowed_resources に HTTPS URL として登録する。Authorize request では登録済み resource を指定する。未指定の場合、Access Token の audience は issuer URL となる。指定した場合はその resource URL が Access Token の aud になる。複数 audience は同じ query/body field を繰り返して送る。

Authorization Code exchange の resource は authorize transaction に保存された集合を狭められるだけである。Refresh 時にも元の family にない resource を加えられない。拡張要求は invalid_scope で拒否される。API は必ず自分の resource URL を aud で確認する。

UserInfo#

GET $AUTH_ISSUER/oauth2/userinfo に Authorization: Bearer <access_token> を付ける。Access Token は openid scope を含まなければならず、UserInfo は常に sub を返す。profile と email claims はそれぞれ該当 scope が付与された場合だけ返る。

curl -sS "$AUTH_ISSUER/oauth2/userinfo" \
  -H "authorization: Bearer $AUTH_ACCESS_TOKEN"

Response の例:

{
  "sub": "<userId>",
  "email": "<email>",
  "email_verified": true,
  "updated_at": 1790664104
}

無効・期限切れ・logout 済み Access Token は 401 invalid_token Problem Details になる。openid がない場合は 400 invalid_request になる。UserInfo は security version と sid の live session も確認する。

Introspection#

POST $AUTH_ISSUER/oauth2/introspect の token は必須。token_type_hint は access_token または refresh_token を指定できる。呼び出し元は web_bff Client の client_secret_basic / client_secret_post 認証、または introspect Scope を付与された Management API key (X-Management-API-Key: <secret>) を使う。environment_id を設定して Environment に固定した key は、issuer の <environmentId> と environment_id が一致する場合に有効である。environment_id を省略した Workspace-scoped key は同じ Workspace の Environment で使える。管理キーを発行するには、Management API の scope introspect を指定する。認証・Scope・Environment が一致しない呼び出し元には token の存在を明かさず {"active":false} を返す。Introspection は署名・期限・issuer が有効な Access Token を active と判定し、Refresh Token は active と判定しない。

curl -sS "$AUTH_ISSUER/oauth2/introspect" \
  -u "$AUTH_CLIENT_ID:$AUTH_CLIENT_SECRET" \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode "token=$AUTH_ACCESS_TOKEN" \
  --data-urlencode 'token_type_hint=access_token'

active: true の response は scope, client_id, token_type, exp, iat, sub, aud, iss, jti などを含む。検証できない token、inactive token、権限のない呼び出し元には token の存在を明かさず {"active":false} を返す。Introspection は User の block 状態、security version、session 失効も確認する。

Revocation#

POST $AUTH_ISSUER/oauth2/revoke は token と任意の token_type_hint (access_token / refresh_token) を受け付ける。token_type_hint に関わらず refresh-token wire format を判別して処理する。Refresh Token の revoke は family 全体を失効する。存在しない token、形式不正 token、Access Token は 200 {} となるため、token の存在を response から推測できない。Access Token 自体は denylist に追加されないため、Access Token の revoke は即時失効させない。

curl -i "$AUTH_ISSUER/oauth2/revoke" \
  -H 'content-type: application/x-www-form-urlencoded' \
  --data-urlencode "token=$AUTH_REFRESH_TOKEN" \
  --data-urlencode 'token_type_hint=refresh_token'

Revoke endpoint は client secret / Management API key を要求しない。Refresh Token bearer を保持する client は revoke できる。Storage failure 時は server_error を返す。

RP-Initiated Logout#

Discovery の end_session_endpoint (GET / POST /oauth2/logout) を使う。id_token_hint は任意だが、渡すとその ID Token の sid session を終了する。期限切れ ID Token も logout hint として受け付ける。post_logout_redirect_uri は Client の post_logout_redirect_uris に登録済みの URI と byte-for-byte 一致しなければならず、必須である。client_id は省略時に ID Token の aud から解決される。指定した state は redirect にそのまま返る。

curl -iG "$AUTH_ISSUER/oauth2/logout" \
  --data-urlencode "id_token_hint=$AUTH_ID_TOKEN" \
  --data-urlencode "post_logout_redirect_uri=$AUTH_POST_LOGOUT_REDIRECT_URI" \
  --data-urlencode "state=$LOGOUT_STATE"

成功時は 302 redirect となり、登録済み URI の既存 query を保ったまま state が追加される。post_logout_redirect_uri が必須のため、成功時は常に redirect する。id_token_hint、client_id、または redirect URI が不正な場合は 400 invalid_request となり、未登録 redirect へは遷移しない。Logout で session が終了しても Access Token の署名・exp は変わらないが、UserInfo / introspection はその token を inactive とする。

Redirect URI#

Login の redirect_uri は登録値と byte-for-byte 完全一致させる。scheme、host、port、path、query の正規化や trailing slash の差分は一致扱いにならない。Fragment (#...) と wildcard (*) は禁止する。

Client type 許可される redirect
web_bff, web_spa HTTPS。HTTP は hostname が localhost または 127.0.0.1 の場合だけ許可
react_native HTTPS Universal Link / App Link、または reverse-DNS custom scheme。例: com.customer.app:/oauth/callback

IPv6 loopback [::1] は localhost の例外に含まれない。React Native で HTTP URI と reverse-DNS でない custom scheme は使えない。Login redirect と post-logout redirect はそれぞれ client の redirect_uris / post_logout_redirect_uris に事前登録する。Callback では state を元の値と比較し、認証レスポンスを処理する前に CSRF を防ぐ。

OAuth error response#

OAuth endpoint の JSON error は { "error": "...", "error_description": "..." } の形で返る。主な error と発生条件は次のとおりである。

Error Typical status 発生例
invalid_request 400 malformed authorize/token request、未対応の response_type / PKCE method / grant_type
invalid_client 401 BFF secret 欠落・不正、public client で secret を送信
invalid_grant 400 無効・期限切れ・使用済み code、期限切れ / 失効済み / replay 済み refresh token
invalid_scope 400 Refresh 時に元の grant を超える scope/resource を要求
access_denied 403 block 済み user
server_error 503 endpoint の repository、key、storage などの server-side failure

Response contract には unsupported_grant_type と temporarily_unavailable も定義されるが、未対応 grant_type は invalid_request を返す。/userinfo の bearer failure は OAuth JSON ではなく 401 invalid_token Problem Details。/introspect の不正 token / 不十分な caller は 200 {"active":false}、/revoke は通常 200 {} を返す。