Claims Authorization

by DuendeSoftwarefb32edc51982No license9 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 4 weeks ago

Claims transformation and profile service patterns for Duende IdentityServer — IProfileService, IClaimsTransformation, claim type mapping, token claim filtering, extension grant validators, and dynamic claims loading.

Instructions onlySoftware Development
AI-generated overview

Guides claims transformation and profile service patterns in Duende IdentityServer, covering IProfileService, claim mapping and token filtering.

What it does
This reference skill explains how claims flow from external identity providers through IdentityServer into identity tokens, access tokens and the userinfo endpoint. It documents IProfileService and IClaimsTransformation usage, claim type mapping, scope-based claim filtering, extension grant validators and dynamic claims loading, with C# code examples and two linked sub-documents. It produces guidance rather than runnable artifacts.
When to use it
Use it when implementing or customizing IProfileService, mapping claims from external providers such as Google or Azure AD, configuring UserClaims on identity resources and API scopes, or troubleshooting claims that are missing from tokens. It also applies when writing extension grant validators or handling claim type mapping in consuming ASP.NET Core APIs.
Requirements
No scripts or runtime are required; it is instructions and documentation only. The guidance targets Duende IdentityServer and ASP.NET Core projects, and it references two bundled markdown sub-documents.

Claims Transformation & Profile Service

When to Use This Skill

  • You are implementing or customizing IProfileService to 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, or ApiResource UserClaims collections and need to understand how requested scopes drive ProfileDataRequestContext.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 IExtensionGrantValidator and 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, JwtClaimTypes vs. Microsoft ClaimTypes).

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, or ApiResource. Declaring a claim on your user store is not enough — it must be listed in a resource's UserClaims collection and the client must request that resource's scope.
  • IProfileService is the single authoritative extension point for controlling which user claims enter tokens. Do not use IClaimsTransformation on 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 AlwaysIncludeUserClaimsInIdToken sparingly. Prefer the userinfo endpoint for full profile data.
  • AddRequestedClaims respects consent. Use context.AddRequestedClaims(claims) rather than context.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 ClaimValueType correctly (e.g. ClaimValueTypes.Integer64, IdentityServerConstants.ClaimValueTypes.Json) so numeric and structured values arrive in tokens as the right JSON type rather than strings.
  • MapInboundClaims = false is 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), breaking User.FindFirst(JwtClaimTypes.Subject) lookups.

Docs: https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/


Sub-Documents

DocumentDescriptionWhen to Load
docs/extension-grant-claims.md [blocked]IExtensionGrantValidator implementation for custom grant types with claim propagationExtension grants, token exchange, custom grant type, IExtensionGrantValidator, GrantValidationResult
docs/external-provider-claims.md [blocked]External provider login callback with claim mapping, Google/AAD normalization, and ClaimActionsExternal provider, Google, Azure AD, OIDC callback, claim mapping, ExternalCookieAuthenticationScheme

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.

External IdP ──► IdentityServer login callback                     │                     ▼              Cookie principal (ClaimsPrincipal)              – built during SignInAsync              – stored in authentication session                     │                     ▼              IProfileService.GetProfileDataAsync              – called at token issuance time              – selects/augments claims for each token type                     │              ┌──────┴──────┐              ▼             ▼        Identity Token   Access Token        (for client)     (for API)                               │                               ▼                         API JWT bearer handler                         – IClaimsTransformation (optional)                         – MapInboundClaims = false                               │                               ▼                         HttpContext.User                         – used by [Authorize], policies, handlers

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

csharp
// Duende.IdentityServer.Servicespublic interface IProfileService{    // Called to get claims for a token or the userinfo endpoint.    Task GetProfileDataAsync(ProfileDataRequestContext context);
    // Called to check whether the user is still active (e.g. not disabled).    // context.Caller is a ProfileIsActiveCallers constant that tells you WHY    // the check is being made (e.g. AuthorizeEndpoint, Token, RefreshTokenValidation).    Task IsActiveAsync(IsActiveContext context);}

ProfileDataRequestContext Key Members

MemberDescription
SubjectThe ClaimsPrincipal from the authentication session (or from the access token for userinfo calls).
ClientThe Client making the request — use for per-client filtering.
CallerWhat triggered this call: ClaimsProviderAccessToken, ClaimsProviderIdentityToken, UserInfoEndpoint.
RequestedClaimTypesClaim types requested by the client, built from the UserClaims of the resources (IdentityResource/ApiScope/ApiResource) resolved for the request.
IssuedClaimsPopulate this collection with claims to include in the token.
AddRequestedClaims(IEnumerable<Claim>)Helper that filters your claims to only those in RequestedClaimTypes.

Minimal Implementation

csharp
// ✅ Correct: extend DefaultProfileService, use AddRequestedClaimspublic sealed class ApplicationProfileService : DefaultProfileService{    private readonly IUserRepository _users;    private readonly ILogger<ApplicationProfileService> _logger;
    public ApplicationProfileService(        IUserRepository users,        ILogger<ApplicationProfileService> logger)        : base(logger)    {        _users = users;        _logger = logger;    }
    public override async Task GetProfileDataAsync(ProfileDataRequestContext context)    {        // Source claims from Subject (cheap — already in memory)        var subjectId = context.Subject.GetSubjectId();
        // Load additional claims from the database        var user = await _users.FindBySubjectIdAsync(subjectId);        if (user is null)        {            _logger.LogWarning("Profile service: user {SubjectId} not found", subjectId);            return;        }
        var claims = new List<Claim>        {            new(JwtClaimTypes.Name, user.DisplayName),            new(JwtClaimTypes.Email, user.Email),            new("tenant_id", user.TenantId),            new("subscription_tier", user.SubscriptionTier),        };
        // Only emit claims that were requested by the client's scopes        context.AddRequestedClaims(claims);    }
    public override async Task IsActiveAsync(IsActiveContext context)    {        var subjectId = context.Subject.GetSubjectId();        var user = await _users.FindBySubjectIdAsync(subjectId);        context.IsActive = user is { IsEnabled: true };    }}

ProfileIsActiveCallers: IsActiveContext.Caller is a ProfileIsActiveCallers constant 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.

csharp
// Program.csbuilder.Services.AddIdentityServer()    .AddProfileService<ApplicationProfileService>();

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:

csharp
// ✅ Always emit tenant_id, regardless of requested scopespublic override async Task GetProfileDataAsync(ProfileDataRequestContext context){    var subjectId = context.Subject.GetSubjectId();    var user = await _users.FindBySubjectIdAsync(subjectId);
    // Mandatory claim — bypasses scope-based filtering    context.IssuedClaims.Add(new Claim("tenant_id", user.TenantId));
    // Scope-filtered claims    var profileClaims = BuildProfileClaims(user);    context.AddRequestedClaims(profileClaims);}
csharp
// ❌ Wrong: adding all claims directly bypasses consent and scope filteringpublic override Task GetProfileDataAsync(ProfileDataRequestContext context){    // This ignores RequestedClaimTypes and consent — user agreed to share only    // the claims associated with the requested scopes.    context.IssuedClaims.AddRange(GetAllUserClaims());    return Task.CompletedTask;}

Differentiating by Caller

The Caller property lets you tailor claims for each token type:

csharp
public override async Task GetProfileDataAsync(ProfileDataRequestContext context){    var user = await _users.FindBySubjectIdAsync(context.Subject.GetSubjectId());
    if (context.Caller == IdentityServerConstants.ProfileDataCallers.ClaimsProviderIdentityToken)    {        // Identity tokens go to the browser — keep them small        context.IssuedClaims.Add(new Claim(JwtClaimTypes.Name, user.DisplayName));        return;    }
    // Access tokens and userinfo can include richer application claims    var claims = BuildFullClaimSet(user);    context.AddRequestedClaims(claims);}

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:

csharp
public override async Task GetProfileDataAsync(ProfileDataRequestContext context){    // context.Subject.GetSubjectId() works for all callers    var subjectId = context.Subject.GetSubjectId();
    // For userinfo, context.Subject contains access-token claims only —    // not the full session principal. Load from database instead.    var user = await _users.FindBySubjectIdAsync(subjectId);
    context.AddRequestedClaims(BuildProfileClaims(user));}

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:

CallerWhenNotes
ClaimsProviderIdentityTokenBuilding the id_tokenCalled with includeAllIdentityClaims = false → the id_token is minimal by default
ClaimsProviderAccessTokenBuilding the access token
UserInfoEndpointClient calls /connect/userinfoSubject comes from the access token

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; only IsActiveAsync is called.
  • true: GetProfileDataAsync runs 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.
csharp
// ✅ Prefer userinfo for profile data — keep id_token lean// On the client (ASP.NET Core OIDC handler):options.GetClaimsFromUserInfoEndpoint = true;options.SaveTokens = true;

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).

csharp
// ⚠️ Use sparingly — increases id_token size significantlyvar client = new Client{    ClientId = "native_app",    AlwaysIncludeUserClaimsInIdToken = true,    AllowedScopes = { "openid", "profile", "email" },};

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 ApiResource only appear when that resource is requested via resource indicator.

Resource-Based Claim Filtering

Declare claims on ApiResource to scope them to that specific API:

csharp
// ✅ Claims on ApiResource are only emitted when that resource is requestednew ApiResource("invoicing", "Invoicing API"){    Scopes = { "invoicing.read", "invoicing.write" },    UserClaims = { "cost_center", "approval_limit" }  // Only in tokens for this API}
new ApiScope("invoicing.read"){    UserClaims = { "department" }  // Emitted when this scope is requested}

Claim Value Types

Set ClaimValueType to ensure correct JSON serialization in the JWT:

csharp
// ✅ Numeric and boolean claims serialize as JSON primitivesvar claims = new List<Claim>{    new("account_id", "42",        ClaimValueTypes.Integer64),
    new("is_verified", "true",        ClaimValueTypes.Boolean),
    new("permissions", """["read","write"]""",        IdentityServerConstants.ClaimValueTypes.Json),};
csharp
// ❌ Without ClaimValueType, all values serialize as JSON strings// { "account_id": "42" }  ← wrong, should be 42new Claim("account_id", "42")

Claim Types and Mapping

JwtClaimTypes vs. System ClaimTypes

The Duende.IdentityModel (or IdentityModel) library provides JwtClaimTypes with the short JWT/OIDC claim names:

JwtClaimTypesLong Microsoft ClaimTypes
subhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
namehttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
emailhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
rolehttp://schemas.microsoft.com/ws/2008/06/identity/claims/role
given_namehttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname

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:

csharp
// ✅ In your API — keep standard OIDC short namesbuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)    .AddJwtBearer(options =>    {        options.Authority = "https://identity.example.com";        options.Audience = "my_api";        options.MapInboundClaims = false;  // Keep "sub", not the long name    });
csharp
// ✅ In web app OIDC handler — same principlebuilder.Services.AddAuthentication(...)    .AddOpenIdConnect("oidc", options =>    {        options.Authority = "https://identity.example.com";        options.MapInboundClaims = false;        options.TokenValidationParameters.NameClaimType = JwtClaimTypes.Name;        options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role;    });
csharp
// ❌ Without MapInboundClaims = false:// User.FindFirst("sub")  → null// User.FindFirst("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier") → found

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 ClaimsPrincipal with 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.
csharp
// ✅ In an API project — augment principal after token validationpublic sealed class TenantClaimsTransformation : IClaimsTransformation{    private readonly ITenantRepository _tenants;
    public TenantClaimsTransformation(ITenantRepository tenants)    {        _tenants = tenants;    }
    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)    {        var tenantId = principal.FindFirstValue("tenant_id");        if (tenantId is null)        {            return principal;        }
        var tenant = await _tenants.GetByIdAsync(tenantId);        if (tenant is null)        {            return principal;        }
        // Clone before mutating — ClaimsPrincipal can be reused across calls        var identity = new ClaimsIdentity();        identity.AddClaim(new Claim("tenant_name", tenant.DisplayName));        identity.AddClaim(new Claim("tenant_region", tenant.Region));
        foreach (var role in tenant.ApplicationRoles)        {            identity.AddClaim(new Claim(ClaimTypes.Role, role));        }
        principal.AddIdentity(identity);        return principal;    }}
csharp
// Program.cs — in the APIbuilder.Services.AddTransient<IClaimsTransformation, TenantClaimsTransformation>();

Do not use IClaimsTransformation on the IdentityServer host to modify token claims. It runs during cookie sign-in/validation and does not affect token content — use IProfileService there 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 ClaimActions examples.


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.

csharp
public sealed class DynamicProfileService : DefaultProfileService{    private readonly IUserPermissionService _permissions;    private readonly IFeatureFlagService _features;    private readonly ILogger<DynamicProfileService> _logger;
    public DynamicProfileService(        IUserPermissionService permissions,        IFeatureFlagService features,        ILogger<DynamicProfileService> logger)        : base(logger)    {        _permissions = permissions;        _features = features;        _logger = logger;    }
    public override async Task GetProfileDataAsync(ProfileDataRequestContext context)    {        var subjectId = context.Subject.GetSubjectId();
        // Run database calls concurrently        var (permissionsTask, featuresTask) = (            _permissions.GetForUserAsync(subjectId, context.Client.ClientId),            _features.GetEnabledForUserAsync(subjectId)        );
        await Task.WhenAll(permissionsTask, featuresTask);
        var claims = new List<Claim>();
        // Role claims from permission service        foreach (var permission in permissionsTask.Result)        {            claims.Add(new Claim(JwtClaimTypes.Role, permission));        }
        // Feature flag claims — serialize as JSON array        var featuresJson = System.Text.Json.JsonSerializer.Serialize(featuresTask.Result);        claims.Add(new Claim(            "features",            featuresJson,            IdentityServerConstants.ClaimValueTypes.Json));
        context.AddRequestedClaims(claims);    }
    public override async Task IsActiveAsync(IsActiveContext context)    {        // Guard against token use after account suspension        var subjectId = context.Subject.GetSubjectId();        context.IsActive = await _permissions.IsUserActiveAsync(subjectId);    }}

Performance note: GetProfileDataAsync is called on every token issuance, including refresh token redemptions. Use caching (IMemoryCache, IDistributedCache) for expensive lookups, keyed by subjectId + clientId. Cache TTL should be shorter than your access token lifetime.

Caching Dynamic Claims

csharp
public override async Task GetProfileDataAsync(ProfileDataRequestContext context){    var subjectId = context.Subject.GetSubjectId();    var cacheKey = $"profile:{subjectId}:{context.Client.ClientId}";
    if (!_cache.TryGetValue(cacheKey, out IReadOnlyList<Claim>? cachedClaims))    {        cachedClaims = await LoadClaimsFromDatabaseAsync(subjectId, context.Client.ClientId);
        _cache.Set(cacheKey, cachedClaims, TimeSpan.FromMinutes(5));    }
    context.AddRequestedClaims(cachedClaims!);}

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.

csharp
var client = new Client{    ClientId = "billing-service",    ClientSecrets = { new Secret("secret".Sha256()) },    AllowedGrantTypes = GrantTypes.ClientCredentials,    AllowedScopes = { "invoicing.api" },
    // Prefixed as "client_customer_id" in the token    Claims =    {        new ClientClaim("customer_id", "acme-corp"),        new ClientClaim("region", "us-east"),    },
    // Remove the prefix: emit as "customer_id" (use carefully)    // ClientClaimsPrefix = ""};

Client claims are only emitted in the client credentials flow by default. For other flows set AlwaysSendClientClaims = true on the client definition.

For dynamic client claims (e.g. set based on runtime context), implement a custom token request validator:

csharp
public sealed class DynamicClientClaimsValidator : ICustomTokenRequestValidator{    private readonly IClientContextService _clientContext;
    public DynamicClientClaimsValidator(IClientContextService clientContext)    {        _clientContext = clientContext;    }
    public async Task ValidateAsync(CustomTokenRequestValidationContext context)    {        if (context.Result.ValidatedRequest.GrantType != GrantType.ClientCredentials)        {            return;        }
        var clientId = context.Result.ValidatedRequest.Client.ClientId;        var tier = await _clientContext.GetSubscriptionTierAsync(clientId);
        context.Result.ValidatedRequest.ClientClaims.Add(            new Claim("subscription_tier", tier));    }}

Common Pitfalls

Claims Not Appearing in Tokens

  1. Claim not in UserClaims: The claim type must be listed in the UserClaims collection of the IdentityResource, ApiScope, or ApiResource that the client requests.
csharp
// ❌ "department" never requested — won't appear even if profile service emits itnew ApiScope("api.read");  // no UserClaims
// ✅ Declare the claim on the scopenew ApiScope("api.read"){    UserClaims = { "department", "cost_center" }}
  1. Client not requesting the scope: The client must include the scope in AllowedScopes and request it at authorization time.

  2. AddRequestedClaims filtered it out: If you use context.AddRequestedClaims(claims), only claims whose types are in context.RequestedClaimTypes pass through. Check whether the scope was requested.

  3. 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 be GetClaimsFromUserInfoEndpoint = true on the client.

  4. Enable debug logging: turn on Duende.IdentityServer debug logging. The default profile service logs requested vs. issued claim types, which reveals whether a claim was filtered out by RequestedClaimTypes rather 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:

csharp
// ✅ Always set this in APIs consuming IdentityServer tokensoptions.MapInboundClaims = false;

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:

csharp
// ✅ Create a new identity, add to existing principalvar identity = new ClaimsIdentity();identity.AddClaim(new Claim("app_role", "admin"));principal.AddIdentity(identity);return principal;
// ❌ Never mutate the principal's existing identities in-place((ClaimsIdentity)principal.Identity!).AddClaim(new Claim("app_role", "admin"));

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

Related Skills

  • aspnetcore-authorization — policy-based authorization, IAuthorizationRequirement, resource-based authorization using claims in the ClaimsPrincipal
  • identityserver-configuration — configuring IdentityResource, ApiScope, ApiResource, and Client definitions that drive which claims are requested
  • aspnetcore-authentication — cookie authentication, OIDC handler configuration, MapInboundClaims, and GetClaimsFromUserInfoEndpoint

Source and attribution

Source:DuendeSoftware/duende-skillsinskills/claims-authorizationat commitfb32edc

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

Claims Authorization Agent Skill | SourceWeft