Skip to content

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.

ClaimScopeID tokenAccess tokenUserInfoWhat it is
subbasic (always)yesyesyesthe person's stable identifier at this issuer; with iss, the key
nameprofileyesnoyesfull name, for display
preferred_usernameprofileyesnoyesusername at the IdP, for display; not a key
emailemailyesnoyesthe account's email, unique at the IdP; an attribute, not a key
email_verifiedemailyesnoyestrue 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.

The IdP guarantees

email_verified is true only after real verification. An expired link or a failed send verifies no one; a reused link opens no session.

ClaimID tokenAccess tokenWhat it is
issyesyesthe issuer; must equal the configured one
audyesyesyour client_id; in the access token, the token’s destination
exp, iatyesyesexpiry and issue time, seconds since 1970
auth_timeyesyestime of authentication
azpyesyesthe client that requested the token
sidyesyesthe central session the token belongs to
jtiyesyesthe token’s identifier
typIDBearerthe token type, as the current engine emits it
nonceyesnothe value sent in the request
at_hashyesnohash of the access token
scopenoyesthe granted scopes: openid profile email
allowed-originsnoyesonly for a client with webOrigins; the engine’s CORS; your API ignores it

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.

Real payloads from the current engine with the contract applied (values replaced):

ID token (payload)
{
"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
}
Access token (payload)
{
"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 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
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" } }
]
}