Contract
Claims
The closed set of claims IdP Urupema issues, which token carries each one, real token payloads, and what is never issued.
The request openid profile email delivers a closed set. The realm’s basic, profile, email and web-origins scopes carry exactly the mappers of contract-claims.json, reimposed on every deploy: a mapper added through the console is removed; a configuration that grants anything outside the contract is reverted. The realm’s default scopes (what a client created in the console inherits) are the same contract, with no optional scope.
Identity claims
Section titled “Identity claims”| Claim | Scope | ID token | Access token | UserInfo | What it is |
|---|---|---|---|---|---|
sub | basic (always) | yes | yes | yes | the person's stable identifier at this issuer; with iss, the key |
name | profile | yes | no | yes | full name, for display |
preferred_username | profile | yes | no | yes | username at the IdP, for display; not a key |
email | email | yes | no | yes | the account's email, unique at the IdP; an attribute, not a key |
email_verified | email | yes | no | yes | true only after real verification |
name, preferred_username, email and email_verified go in the ID token and the UserInfo response, not in the access token, which carries only sub. An API that needs the name or email calls userinfo_endpoint with the access token it received.
email_verified is true only after real verification. An expired link or a failed send verifies no one; a reused link opens no session.
Protocol claims
Section titled “Protocol claims”| Claim | ID token | Access token | What it is |
|---|---|---|---|
iss | yes | yes | the issuer; must equal the configured one |
aud | yes | yes | your client_id; in the access token, the token’s destination |
exp, iat | yes | yes | expiry and issue time, seconds since 1970 |
auth_time | yes | yes | time of authentication |
azp | yes | yes | the client that requested the token |
sid | yes | yes | the central session the token belongs to |
jti | yes | yes | the token’s identifier |
typ | ID | Bearer | the token type, as the current engine emits it |
nonce | yes | no | the value sent in the request |
at_hash | yes | no | hash of the access token |
scope | no | yes | the granted scopes: openid profile email |
allowed-origins | no | yes | only for a client with webOrigins; the engine’s CORS; your API ignores it |
Never issued
Section titled “Never issued”Engine roles (realm_access, resource_access), given_name, family_name, locale, phone, address, groups, and any app-specific claim. No scope requests them; asking returns invalid_scope.
Payloads
Section titled “Payloads”Real payloads from the current engine with the contract applied (values replaced):
{ "iss": "https://id.institutourupema.com.br/realms/urupema", "sub": "4281c997-dbc7-42e9-8446-726ba0fc2073", "aud": "my-app", "azp": "my-app", "typ": "ID", "exp": 1791166733, "iat": 1791166433, "auth_time": 1791166433, "jti": "7d265562-7b17-b435-15c4-a23f2545e2bf", "nonce": "n-0S6_WzA2Mj", "sid": "ij7Sg1bwli75CIDE0zxoNBdR", "at_hash": "0qPyHaIca4RxraaPhIkmXA", "name": "Ana Souza", "preferred_username": "ana.souza", "email": "ana.souza@example.org", "email_verified": true}{ "iss": "https://id.institutourupema.com.br/realms/urupema", "sub": "4281c997-dbc7-42e9-8446-726ba0fc2073", "aud": "my-app", "azp": "my-app", "typ": "Bearer", "exp": 1791166733, "iat": 1791166433, "auth_time": 1791166433, "jti": "onrtac:3dc67726-7963-380c-7f2e-389661f85351", "sid": "ij7Sg1bwli75CIDE0zxoNBdR", "scope": "openid profile email"}The access token lives 300 seconds (exp − iat). The refresh token is opaque: store it and send it back to the IdP; never parse it. sub is an opaque string, not a UUID.
A new claim
Section titled “A new claim”A claim enters the contract only with an identifiable source of truth, a stable documented meaning, a need of more than one app, and no authorization rule moved into the IdP. A claim one app needs stays in that app.
The source: contract-claims.json
{ "_doc": "IdP claims contract (README §2.3, I-001/INV15). reconcile.ts enforces EXACTLY these mappers on the realm default scopes on every deploy: a mapper not in the list is deleted, diverging config is recreated. A new claim only enters here, under the conditions of README §0. Identity claims go in the ID token and userinfo, not in the access token (the resource calls userinfo if it needs them).", "basic": [ { "name": "sub", "protocolMapper": "oidc-sub-mapper", "config": { "introspection.token.claim": "true", "access.token.claim": "true" } }, { "name": "auth_time", "protocolMapper": "oidc-usersessionmodel-note-mapper", "config": { "user.session.note": "AUTH_TIME", "claim.name": "auth_time", "jsonType.label": "long", "id.token.claim": "true", "access.token.claim": "true", "userinfo.token.claim": "true", "introspection.token.claim": "true" } } ], "profile": [ { "name": "full name", "protocolMapper": "oidc-full-name-mapper", "config": { "id.token.claim": "true", "userinfo.token.claim": "true", "access.token.claim": "false", "introspection.token.claim": "true" } }, { "name": "username", "protocolMapper": "oidc-usermodel-attribute-mapper", "config": { "user.attribute": "username", "claim.name": "preferred_username", "jsonType.label": "String", "id.token.claim": "true", "userinfo.token.claim": "true", "access.token.claim": "false", "introspection.token.claim": "true" } } ], "email": [ { "name": "email", "protocolMapper": "oidc-usermodel-attribute-mapper", "config": { "user.attribute": "email", "claim.name": "email", "jsonType.label": "String", "id.token.claim": "true", "userinfo.token.claim": "true", "access.token.claim": "false", "introspection.token.claim": "true" } }, { "name": "email verified", "protocolMapper": "oidc-usermodel-property-mapper", "config": { "user.attribute": "emailVerified", "claim.name": "email_verified", "jsonType.label": "boolean", "id.token.claim": "true", "userinfo.token.claim": "true", "access.token.claim": "false", "introspection.token.claim": "true" } } ], "web-origins": [ { "name": "allowed web origins", "protocolMapper": "oidc-allowed-origins-mapper", "config": { "introspection.token.claim": "true", "access.token.claim": "true" } } ]}