Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

Claims Lifecycle

Understanding where claims live at each step of a protocol flow is essential for configuring IdentityServer correctly. This page provides sequence diagrams for each major flow, showing:

  • Where claims are stored (session cookie, persisted grants DB, tokens, client cookie, API context)
  • When the Profile Service is invoked
  • How configuration options change claim placement
LocationDescription
IdentityServer SessionCookie storing sub, name, amr, auth_time, idp, sid after login
Persisted Grants DBStores authorization codes, refresh tokens, and reference access tokens with associated claims
Identity TokenJWT sent to the client describing the authentication event
Access TokenJWT or reference token authorizing API access, contains scope-based claims
UserInfo EndpointReturns identity claims on demand when presented with a valid access token
Client Session (Cookie)ASP.NET Core auth cookie storing claims the client chose to persist
API HttpContextClaims principal populated from the validated access token

Authorization Code with UserInfo Recommended

Section titled “Authorization Code with UserInfo ”

This is the recommended default. The id_token is minimal (just sub), and the client retrieves full identity claims from the userinfo endpoint. This keeps tokens small and is the standard behavior for confidential clients.

sequenceDiagram
    participant B as Browser
    participant C as Client App
    participant IS as IdentityServer
    participant PS as Profile Service

    B->>IS: GET /authorize (response_type=code)
    IS-->>B: Redirect with authorization code
    B->>C: Authorization code
    C->>IS: POST /token (grant_type=authorization_code)
    IS->>PS: GetProfileData<br/>(Caller=IdentityToken,<br/>includeAllIdentityClaims=false)
    PS-->>IS: sub (minimal)
    IS->>PS: GetProfileData<br/>(Caller=AccessToken)
    PS-->>IS: sub, scope-based claims
    IS-->>C: id_token (sub only) + access_token
    C->>IS: GET /connect/userinfo (Bearer token)
    IS->>PS: GetProfileData<br/>(Caller=UserInfoEndpoint)
    PS-->>IS: name, email, roles...
    IS-->>C: JSON { name, email, ... }
    Note over C: Client cookie stores:<br/>sub + userinfo response claims

Key points:

  • Profile Service is called 3 times with different Caller values
  • The id_token is small (reduces redirect URL size)
  • Identity claims come from userinfo, not the id_token
  • The ASP.NET Core OIDC handler does this automatically when GetClaimsFromUserInfoEndpoint = true

When AlwaysIncludeUserClaimsInIdToken = true on the client, identity claims are placed directly in the id_token even though an access token is available. This avoids the extra round-trip to userinfo.

sequenceDiagram
    participant B as Browser
    participant C as Client App
    participant IS as IdentityServer
    participant PS as Profile Service

    B->>IS: GET /authorize (response_type=code)
    IS-->>B: Redirect with authorization code
    B->>C: Authorization code
    C->>IS: POST /token (grant_type=authorization_code)
    IS->>PS: GetProfileData<br/>(Caller=IdentityToken,<br/>includeAllIdentityClaims=true)
    PS-->>IS: sub, name, email, roles...
    IS->>PS: GetProfileData<br/>(Caller=AccessToken)
    PS-->>IS: sub, scope-based claims
    IS-->>C: id_token + access_token
    Note over C: id_token has ALL identity claims<br/>access_token has API-scoped claims
    Note over C: Client cookie stores id_token claims<br/>No userinfo call needed

Configuration: Set AlwaysIncludeUserClaimsInIdToken = true on the Client to enable this behavior.

When to use: When your client doesn’t want to make an extra HTTP call to userinfo, or when you need claims immediately available at sign-in without a round-trip.


When a client refreshes an access token, claims may be reloaded from the Profile Service or reused from the original grant. The behavior depends on the UpdateAccessTokenClaimsOnRefresh client setting.

The new access token reuses claims from the original authorization. The Profile Service is only called to verify the user is still active.

sequenceDiagram
    participant C as Client
    participant IS as IdentityServer
    participant DB as Persisted Grants DB
    participant PS as Profile Service

    C->>IS: POST /token (grant_type=refresh_token)
    IS->>DB: Load refresh token grant
    DB-->>IS: Original subject + claims metadata
    IS->>PS: IsActiveAsync (check user still valid)
    PS-->>IS: active=true
    Note over IS: Reuse claims from<br/>original authorization
    IS-->>C: New access_token with ORIGINAL claims<br/>(+ new refresh_token if rotated)

This is the default behavior when UpdateAccessTokenClaimsOnRefresh = false.


With JWT access tokens, the API validates the token locally. There is no backchannel call to IdentityServer. Claims are frozen at the time the token was issued.

sequenceDiagram
    participant C as Client
    participant API as API (Resource Server)
    participant IS as IdentityServer

    Note over C: Has JWT access_token with:<br/>sub, client_id, scope, aud,<br/>custom claims, exp, iss
    C->>API: GET /resource (Authorization: Bearer {jwt})
    API->>API: Validate JWT signature<br/>(using cached JWKS from IS)
    API->>API: Check exp, iss, aud
    API->>API: Extract claims → HttpContext.User
    API-->>C: 200 OK (resource data)
    Note over API: No call to IdentityServer!<br/>Claims reflect state at issuance time

Key points:

  • The API never contacts IdentityServer during request processing
  • Claims are frozen at token issuance. If a user’s role changes, the old token still has the old claims until it expires
  • Keep JWT lifetimes short (5-15 min) and use refresh tokens for longevity
  • The API gets its JWKS keys from the discovery document (cached)

With reference tokens, the API must call IdentityServer’s introspection endpoint on every request to resolve the opaque token handle.

sequenceDiagram
    participant C as Client
    participant API as API (Resource Server)
    participant IS as IdentityServer
    participant DB as Persisted Grants DB
    participant PS as Profile Service

    Note over C: Has opaque reference token handle
    C->>API: GET /resource (Authorization: Bearer {handle})
    API->>IS: POST /connect/introspect<br/>(token={handle}, client_id, client_secret)
    IS->>DB: Lookup token by handle
    DB-->>IS: Token data + claims
    IS->>PS: IsActiveAsync (validate user)
    PS-->>IS: active=true
    IS-->>API: { active: true, sub, scope,<br/>client_id, claims... }
    API->>API: Build ClaimsPrincipal from response
    API-->>C: 200 OK (resource data)

Key points:

  • Every API request triggers an introspection call (cache where appropriate)
  • Reference tokens can be revoked immediately by deleting from the grants DB
  • The API must have an ApiSecret configured on its ApiResource for introspection
  • Claims reflect the stored state (from original issuance unless updated)

In the implicit flow, ALL identity claims go directly into the id_token because no access token is issued (so no userinfo endpoint is available).

sequenceDiagram
    participant B as Browser
    participant IS as IdentityServer
    participant PS as Profile Service
    participant C as Client App

    B->>IS: GET /authorize (response_type=id_token)
    Note over IS: Session cookie: sub, name,<br/>amr, auth_time, idp, sid
    IS->>PS: GetProfileData<br/>(Caller=IdentityToken,<br/>includeAllIdentityClaims=true)
    PS-->>IS: sub, name, email, roles...
    IS-->>B: Redirect with id_token (fragment)
    Note over B: id_token contains ALL<br/>identity resource claims
    B->>C: POST id_token
    Note over C: Client cookie stores claims<br/>from id_token (sub, name, email...)

Key points:

  • includeAllIdentityClaims is true because there’s no access token to use at the userinfo endpoint
  • The Profile Service is called once
  • All identity resource claims are packed into the id_token
  • Tokens in URL fragments are visible in browser history and logs. Prefer authorization code flow with PKCE

OptionDefaultEffectRelevant Flows
AlwaysIncludeUserClaimsInIdTokenfalseWhen true, all identity claims go in id_token (skips userinfo)Code flow
UpdateAccessTokenClaimsOnRefreshfalseWhen true, Profile Service is re-invoked on refreshRefresh
AccessTokenTypeJwtJwt = self-contained, Reference = opaque + introspectionAPI calls
AlwaysSendClientClaimsfalseInclude client claims in all flows (not just client credentials)All flows
ClientClaimsPrefix"client_"Prefix added to client claims in access tokensAll flows