Claims Transformation & Profile Service
When to Use This Skill
- You are implementing or customizing
IProfileServiceto control which claims are emitted into identity tokens, access tokens, or the userinfo endpoint. - You need to map claims from an external identity provider (Google, Azure AD, SAML, etc.) into your IdentityServer user principal during login callback processing.
- You are configuring
IdentityResource,ApiScope, orApiResourceUserClaimscollections and need to understand how requested scopes driveProfileDataRequestContext.RequestedClaimTypes. - You are troubleshooting missing claims — claims are defined on resources but not appearing in tokens or on the userinfo endpoint.
- You need to load claims dynamically from a database or downstream service at token issuance time.
- You are implementing an
IExtensionGrantValidatorand need to emit custom claims into the resulting access token. - You are consuming tokens in an ASP.NET Core API or web app and need to handle claim type mapping (
MapInboundClaims,JwtClaimTypesvs. MicrosoftClaimTypes).
Core Principles
- Claims are opt-in by scope. IdentityServer only asks your profile service for claims that have been declared on a requested
IdentityResource,ApiScope, orApiResource. Declaring a claim on your user store is not enough — it must be listed in a resource'sUserClaimscollection and the client must request that resource's scope. IProfileServiceis the single authoritative extension point for controlling which user claims enter tokens. Do not useIClaimsTransformationon the IdentityServer host to modify token claims — that interface runs during cookie authentication, not token issuance.- Identity tokens are for the client; access tokens are for APIs. Keep identity tokens small. Use
AlwaysIncludeUserClaimsInIdTokensparingly. Prefer the userinfo endpoint for full profile data. AddRequestedClaimsrespects consent. Usecontext.AddRequestedClaims(claims)rather thancontext.IssuedClaims.AddRange(claims)when you want IdentityServer to filter your claims down to only those that were requested and consented to by the user.- Claim serialization is type-aware. Set
ClaimValueTypecorrectly (e.g.ClaimValueTypes.Integer64,IdentityServerConstants.ClaimValueTypes.Json) so numeric and structured values arrive in tokens as the right JSON type rather than strings. MapInboundClaims = falseis required in consuming APIs and web apps. Without it, the JWT bearer handler silently renames standard OIDC claims (e.g.sub→http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier), breakingUser.FindFirst(JwtClaimTypes.Subject)lookups.
Docs: https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/
Sub-Documents
Claims Pipeline Overview
Claims travel through several distinct stages between the user's identity and an API's authorization check. Understanding where each transformation occurs prevents duplicate work and subtle bugs.
Stage 1 — Login callback: Claims from the external provider (or local user store) are incorporated into the IdentityServerUser and persisted in the session cookie. This is where you map external IdP claims to internal claim types.
Stage 2 — Token issuance: When a client requests a token, IdentityServer calls IProfileService.GetProfileDataAsync. The ProfileDataRequestContext tells you which claims are requested (derived from scopes/resources) and what token type is being built. This is where you load dynamic claims from your database.
Stage 3 — Token consumption: APIs receive the JWT and validate it. IClaimsTransformation can augment the ClaimsPrincipal after validation — useful for adding application-specific roles or denormalized data that doesn't belong in the token itself.
IProfileService
IProfileService is the primary extensibility point for claims in Duende IdentityServer. Register your implementation with AddProfileService<T>() during startup.
Interface Contract
ProfileDataRequestContext Key Members
Minimal Implementation
ProfileIsActiveCallers:IsActiveContext.Calleris aProfileIsActiveCallersconstant indicating why the check is being made — e.g.AuthorizeEndpoint,Token,RefreshTokenValidation,UserInfoRequestValidation. Use it to apply different strictness levels; for example, you might allow a soft-disabled account to complete an in-flight refresh but deny new interactive logins.
Emitting Claims Unconditionally
Use context.IssuedClaims.AddRange(...) when a claim must always appear regardless of requested scopes — for example, a mandatory tenant_id that APIs rely on for multi-tenancy:
Differentiating by Caller
The Caller property lets you tailor claims for each token type:
Detecting Userinfo Endpoint Calls
When called for the userinfo endpoint, Subject is populated from the access token rather than the session principal. Guard against assuming session-only data is available:
Profile Service Invocation Count (Lifecycle)
For an authorization-code + userinfo flow, GetProfileDataAsync can be called up to three times per login, distinguished by context.Caller:
The ASP.NET Core OIDC handler fetches userinfo only when options.GetClaimsFromUserInfoEndpoint = true. Setting Client.AlwaysIncludeUserClaimsInIdToken = true puts all identity claims in the id_token and skips the userinfo round-trip.
Refresh Token Claim Updates
On refresh, Client.UpdateAccessTokenClaimsOnRefresh (default false) controls whether GetProfileDataAsync is re-invoked for fresh access-token claims:
false(default): the original claims are reused; onlyIsActiveAsyncis called.true:GetProfileDataAsyncruns again so access-token claims reflect current state.
Claims in Tokens: Identity vs. Access
Identity Token
- Purpose: tells the client application what happened during authentication.
- Audience: the client application only — never send to an API.
- Keep small: the client validates it immediately; large tokens stress browsers and PKCE flows.
- Standard claims:
sub,auth_time,amr,idp,sid,nonce. - User profile claims (name, email) are typically fetched via userinfo rather than embedded.
AlwaysIncludeUserClaimsInIdToken
Setting AlwaysIncludeUserClaimsInIdToken = true on a client forces all profile claims into the identity token, bypassing the userinfo endpoint. Use only when the client cannot make the userinfo call (e.g. native apps with no back-channel).
Access Token
- Purpose: authorizes API calls.
- Audience: the resource server (API).
- Contains:
sub,client_id,scope,jti,iss,exp, + any user claims from profile service. - Resource-based filtering applies: claims associated with a specific
ApiResourceonly appear when that resource is requested via resource indicator.
Resource-Based Claim Filtering
Declare claims on ApiResource to scope them to that specific API:
Claim Value Types
Set ClaimValueType to ensure correct JSON serialization in the JWT:
Claim Types and Mapping
JwtClaimTypes vs. System ClaimTypes
The Duende.IdentityModel (or IdentityModel) library provides JwtClaimTypes with the short JWT/OIDC claim names:
Always use JwtClaimTypes constants in IdentityServer code and in APIs that validate JWTs directly.
MapInboundClaims = false (Required in APIs)
The default JWT bearer handler maps short JWT claim names to long Microsoft WS-Federation names. Disable this:
IClaimsTransformation (API-Side)
IClaimsTransformation is an ASP.NET Core interface that runs in consuming applications after authentication but before authorization. Use it in APIs and web apps — never in the IdentityServer host itself for token content.
When to Use IClaimsTransformation
- Enriching the
ClaimsPrincipalwith application-specific roles from a local database, after validating a token from IdentityServer. - Mapping external department/group memberships to application roles without putting that data in the token.
- Adding denormalized claims (e.g. resolved tenant name from
tenant_id) for use in authorization policies.
Do not use
IClaimsTransformationon the IdentityServer host to modify token claims. It runs during cookie sign-in/validation and does not affect token content — useIProfileServicethere instead.
Extension Grant Validators
IExtensionGrantValidator handles custom OAuth grant types at the token endpoint (e.g. token exchange, assertion grants). Implement ValidateAsync to validate the incoming token/assertion, then call new GrantValidationResult(subject, grantType, customClaims). IProfileService is called subsequently and can augment claims further. Register with AddExtensionGrantValidator<T>().
See docs/extension-grant-claims.md [blocked] for a full token exchange validator implementation with error handling and custom claim propagation.
Claims from External Providers
When a user authenticates through an external provider, IdentityServer receives claims in a temporary external cookie. In the login callback: read via HttpContext.AuthenticateAsync(ExternalCookieAuthenticationScheme), extract the provider user ID, find or provision the local user, build an IdentityServerUser with AdditionalClaims = MapProviderClaims(...), then call SignInAsync + SignOutAsync for the external cookie. For OIDC handlers, use ClaimActions.Clear() followed by explicit MapJsonKey calls to whitelist only the claims you need.
See docs/external-provider-claims.md [blocked] for the full callback controller implementation with Google/AAD claim mapping and
ClaimActionsexamples.
Dynamic Claims Loading
Loading claims dynamically at token issuance time — rather than storing them in the session cookie — keeps your session lean and ensures claims reflect the current state of your database. This is the recommended pattern for role assignments and feature flags that change frequently.
Performance note:
GetProfileDataAsyncis called on every token issuance, including refresh token redemptions. Use caching (IMemoryCache,IDistributedCache) for expensive lookups, keyed bysubjectId + clientId. Cache TTL should be shorter than your access token lifetime.
Caching Dynamic Claims
Client Claims
Client claims are static claims attached to a Client definition and emitted into access tokens. They are prefixed with client_ by default to prevent collision with user claims.
Client claims are only emitted in the client credentials flow by default. For other flows set
AlwaysSendClientClaims = trueon the client definition.
For dynamic client claims (e.g. set based on runtime context), implement a custom token request validator:
Common Pitfalls
Claims Not Appearing in Tokens
- Claim not in
UserClaims: The claim type must be listed in theUserClaimscollection of theIdentityResource,ApiScope, orApiResourcethat the client requests.
-
Client not requesting the scope: The client must include the scope in
AllowedScopesand request it at authorization time. -
AddRequestedClaimsfiltered it out: If you usecontext.AddRequestedClaims(claims), only claims whose types are incontext.RequestedClaimTypespass through. Check whether the scope was requested. -
Check the userinfo endpoint first: the identity token is minimal by default, so a "missing" claim is often available at
/connect/userinfo. Verify there before modifying the profile service — the fix may just beGetClaimsFromUserInfoEndpoint = trueon the client. -
Enable debug logging: turn on
Duende.IdentityServerdebug logging. The default profile service logs requested vs. issued claim types, which reveals whether a claim was filtered out byRequestedClaimTypesrather than never emitted.
Adding a claim to the user/subject is not enough. To surface a new claim you must: (1) define a resource whose UserClaims include it (e.g. new IdentityResource("department_info", ["department"], "Your department")), (2) add that scope to the client's AllowedScopes, and (3) have the request include that scope. Adding a claim directly to IssuedClaims bypasses this filter — do that only when a claim must always be sent.
Wrong Claim Names in APIs
Caused by not setting MapInboundClaims = false. The JWT bearer handler renames sub to the long WS-Federation URI. Fix:
Mutating ClaimsPrincipal in IClaimsTransformation
ClaimsPrincipal instances can be cached and reused. Always create a new ClaimsIdentity and add it to the principal rather than mutating an existing identity:
AlwaysIncludeUserClaimsInIdToken Overuse
Setting AlwaysIncludeUserClaimsInIdToken = true embeds all profile claims in the identity token. This:
- Increases token size (can exceed header/cookie limits).
- Caches profile data in the client until the token expires (stale claims).
- Bypasses the userinfo endpoint's on-demand freshness.
Prefer options.GetClaimsFromUserInfoEndpoint = true in the client OIDC handler.
Storing Too Many Claims in the Session Cookie
The IdentityServer session cookie stores the ClaimsPrincipal from SignInAsync. Large claim sets (e.g. hundreds of AD groups) bloat this cookie, breaking requests with 431 or 400 errors. Keep the session principal minimal — load bulk claims dynamically in IProfileService instead.
Forgetting IsActiveAsync
IsActiveAsync is called on refresh token redemption. If you block token issuance via context.IsActive = false but don't revoke the refresh token, the user sees token request failures without a helpful error. Ensure your user deactivation flow also revokes persisted grants.
Resources
- Duende IdentityServer — Claims fundamentals
- Duende IdentityServer — Profile Service reference
- Duende IdentityServer — Identity Resources
- Duende IdentityServer — API Scopes
- Duende IdentityServer — API Resources
- Duende IdentityServer — Extension Grants
- Duende IdentityServer — External Providers
- Duende IdentityServer — Token types overview
- Duende IdentityServer — Custom Token Request Validator
- ASP.NET Core — IClaimsTransformation
- OpenID Connect Core spec — Standard scope/claim mappings
Related Skills
aspnetcore-authorization— policy-based authorization,IAuthorizationRequirement, resource-based authorization using claims in theClaimsPrincipalidentityserver-configuration— configuringIdentityResource,ApiScope,ApiResource, andClientdefinitions that drive which claims are requestedaspnetcore-authentication— cookie authentication, OIDC handler configuration,MapInboundClaims, andGetClaimsFromUserInfoEndpoint


