← Duende SkillsCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Duende Skills
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "claims-authorization",
"description": "Claims transformation and profile service patterns for Duende IdentityServer — IProfileService, IClaimsTransformation, claim type mapping, token claim filtering, extension grant validators, and dynamic claims loading.",
"included_files": [
{
"relative_path": "docs/extension-grant-claims.md",
"size_in_bytes": 3144
},
{
"relative_path": "docs/external-provider-claims.md",
"size_in_bytes": 4490
}
],
"skill_md_contents": "---\nname: claims-authorization\ndescription: Claims transformation and profile service patterns for Duende IdentityServer — IProfileService, IClaimsTransformation, claim type mapping, token claim filtering, extension grant validators, and dynamic claims loading.\ninvocable: false\n---\n\n# Claims Transformation & Profile Service\n\n## When to Use This Skill\n\n- You are implementing or customizing `IProfileService` to control which claims are emitted into identity tokens, access tokens, or the userinfo endpoint.\n- You need to map claims from an external identity provider (Google, Azure AD, SAML, etc.) into your IdentityServer user principal during login callback processing.\n- You are configuring `IdentityResource`, `ApiScope`, or `ApiResource` `UserClaims` collections and need to understand how requested scopes drive `ProfileDataRequestContext.RequestedClaimTypes`.\n- You are troubleshooting missing claims — claims are defined on resources but not appearing in tokens or on the userinfo endpoint.\n- You need to load claims dynamically from a database or downstream service at token issuance time.\n- You are implementing an `IExtensionGrantValidator` and need to emit custom claims into the resulting access token.\n- 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`).\n\n## Core Principles\n\n- **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.\n- **`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.\n- **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.\n- **`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.\n- **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.\n- **`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.\n\nDocs: https://docs.duendesoftware.com/identityserver/apis/aspnetcore/authorization/\n\n---\n\n## Sub-Documents\n\n| Document | Description | When to Load |\n|----------|-------------|--------------|\n| [docs/extension-grant-claims.md](docs/extension-grant-claims.md) | `IExtensionGrantValidator` implementation for custom grant types with claim propagation | Extension grants, token exchange, custom grant type, IExtensionGrantValidator, GrantValidationResult |\n| [docs/external-provider-claims.md](docs/external-provider-claims.md) | External provider login callback with claim mapping, Google/AAD normalization, and ClaimActions | External provider, Google, Azure AD, OIDC callback, claim mapping, ExternalCookieAuthenticationScheme |\n\n---\n\n## Claims Pipeline Overview\n\nClaims 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.\n\n```\nExternal IdP ──► IdentityServer login callback\n │\n ▼\n Cookie principal (ClaimsPrincipal)\n – built during SignInAsync\n – stored in authentication session\n │\n ▼\n IProfileService.GetProfileDataAsync\n – called at token issuance time\n – selects/augments claims for each token type\n │\n ┌──────┴──────┐\n ▼ ▼\n Identity Token Access Token\n (for client) (for API)\n │\n ▼\n API JWT bearer handler\n – IClaimsTransformation (optional)\n – MapInboundClaims = false\n │\n ▼\n HttpContext.User\n – used by [Authorize], policies, handlers\n```\n\n**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.\n\n**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.\n\n**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.\n\n---\n\n## IProfileService\n\n`IProfileService` is the primary extensibility point for claims in Duende IdentityServer. Register your implementation with `AddProfileService<T>()` during startup.\n\n### Interface Contract\n\n```csharp\n// Duende.IdentityServer.Services\npublic interface IProfileService\n{\n // Called to get claims for a token or the userinfo endpoint.\n Task GetProfileDataAsync(ProfileDataRequestContext context);\n\n // Called to check whether the user is still active (e.g. not disabled).\n // context.Caller is a ProfileIsActiveCallers constant that tells you WHY\n // the check is being made (e.g. AuthorizeEndpoint, Token, RefreshTokenValidation).\n Task IsActiveAsync(IsActiveContext context);\n}\n```\n\n### ProfileDataRequestContext Key Members\n\n| Member | Description |\n|---|---|\n| `Subject` | The `ClaimsPrincipal` from the authentication session (or from the access token for userinfo calls). |\n| `Client` | The `Client` making the request — use for per-client filtering. |\n| `Caller` | What triggered this call: `ClaimsProviderAccessToken`, `ClaimsProviderIdentityToken`, `UserInfoEndpoint`. |\n| `RequestedClaimTypes` | Claim types requested by the client, built from the `UserClaims` of the resources (`IdentityResource`/`ApiScope`/`ApiResource`) resolved for the request. |\n| `IssuedClaims` | Populate this collection with claims to include in the token. |\n| `AddRequestedClaims(IEnumerable<Claim>)` | Helper that filters your claims to only those in `RequestedClaimTypes`. |\n\n### Minimal Implementation\n\n```csharp\n// ✅ Correct: extend DefaultProfileService, use AddRequestedClaims\npublic sealed class ApplicationProfileService : DefaultProfileService\n{\n private readonly IUserRepository _users;\n private readonly ILogger<ApplicationProfileService> _logger;\n\n public ApplicationProfileService(\n IUserRepository users,\n ILogger<ApplicationProfileService> logger)\n : base(logger)\n {\n _users = users;\n _logger = logger;\n }\n\n public override async Task GetProfileDataAsync(ProfileDataRequestContext context)\n {\n // Source claims from Subject (cheap — already in memory)\n var subjectId = context.Subject.GetSubjectId();\n\n // Load additional claims from the database\n var user = await _users.FindBySubjectIdAsync(subjectId);\n if (user is null)\n {\n _logger.LogWarning(\"Profile service: user {SubjectId} not found\", subjectId);\n return;\n }\n\n var claims = new List<Claim>\n {\n new(JwtClaimTypes.Name, user.DisplayName),\n new(JwtClaimTypes.Email, user.Email),\n new(\"tenant_id\", user.TenantId),\n new(\"subscription_tier\", user.SubscriptionTier),\n };\n\n // Only emit claims that were requested by the client's scopes\n context.AddRequestedClaims(claims);\n }\n\n public override async Task IsActiveAsync(IsActiveContext context)\n {\n var subjectId = context.Subject.GetSubjectId();\n var user = await _users.FindBySubjectIdAsync(subjectId);\n context.IsActive = user is { IsEnabled: true };\n }\n}\n```\n\n> **`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.\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n .AddProfileService<ApplicationProfileService>();\n```\n\n### Emitting Claims Unconditionally\n\nUse `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:\n\n```csharp\n// ✅ Always emit tenant_id, regardless of requested scopes\npublic override async Task GetProfileDataAsync(ProfileDataRequestContext context)\n{\n var subjectId = context.Subject.GetSubjectId();\n var user = await _users.FindBySubjectIdAsync(subjectId);\n\n // Mandatory claim — bypasses scope-based filtering\n context.IssuedClaims.Add(new Claim(\"tenant_id\", user.TenantId));\n\n // Scope-filtered claims\n var profileClaims = BuildProfileClaims(user);\n context.AddRequestedClaims(profileClaims);\n}\n```\n\n```csharp\n// ❌ Wrong: adding all claims directly bypasses consent and scope filtering\npublic override Task GetProfileDataAsync(ProfileDataRequestContext context)\n{\n // This ignores RequestedClaimTypes and consent — user agreed to share only\n // the claims associated with the requested scopes.\n context.IssuedClaims.AddRange(GetAllUserClaims());\n return Task.CompletedTask;\n}\n```\n\n### Differentiating by Caller\n\nThe `Caller` property lets you tailor claims for each token type:\n\n```csharp\npublic override async Task GetProfileDataAsync(ProfileDataRequestContext context)\n{\n var user = await _users.FindBySubjectIdAsync(context.Subject.GetSubjectId());\n\n if (context.Caller == IdentityServerConstants.ProfileDataCallers.ClaimsProviderIdentityToken)\n {\n // Identity tokens go to the browser — keep them small\n context.IssuedClaims.Add(new Claim(JwtClaimTypes.Name, user.DisplayName));\n return;\n }\n\n // Access tokens and userinfo can include richer application claims\n var claims = BuildFullClaimSet(user);\n context.AddRequestedClaims(claims);\n}\n```\n\n### Detecting Userinfo Endpoint Calls\n\nWhen 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:\n\n```csharp\npublic override async Task GetProfileDataAsync(ProfileDataRequestContext context)\n{\n // context.Subject.GetSubjectId() works for all callers\n var subjectId = context.Subject.GetSubjectId();\n\n // For userinfo, context.Subject contains access-token claims only —\n // not the full session principal. Load from database instead.\n var user = await _users.FindBySubjectIdAsync(subjectId);\n\n context.AddRequestedClaims(BuildProfileClaims(user));\n}\n```\n\n### Profile Service Invocation Count (Lifecycle)\n\nFor an authorization-code + userinfo flow, `GetProfileDataAsync` can be called **up to three times** per login, distinguished by `context.Caller`:\n\n| Caller | When | Notes |\n|---|---|---|\n| `ClaimsProviderIdentityToken` | Building the id_token | Called with `includeAllIdentityClaims = false` → the id_token is **minimal** by default |\n| `ClaimsProviderAccessToken` | Building the access token | |\n| `UserInfoEndpoint` | Client calls `/connect/userinfo` | `Subject` comes from the access token |\n\nThe 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.\n\n### Refresh Token Claim Updates\n\nOn refresh, `Client.UpdateAccessTokenClaimsOnRefresh` (default `false`) controls whether `GetProfileDataAsync` is re-invoked for fresh access-token claims:\n\n- `false` (default): the original claims are reused; only `IsActiveAsync` is called.\n- `true`: `GetProfileDataAsync` runs again so access-token claims reflect current state.\n\n---\n\n## Claims in Tokens: Identity vs. Access\n\n### Identity Token\n\n- Purpose: tells the client application what happened during authentication.\n- Audience: the client application only — **never send to an API**.\n- Keep small: the client validates it immediately; large tokens stress browsers and PKCE flows.\n- Standard claims: `sub`, `auth_time`, `amr`, `idp`, `sid`, `nonce`.\n- User profile claims (name, email) are typically fetched via userinfo rather than embedded.\n\n```csharp\n// ✅ Prefer userinfo for profile data — keep id_token lean\n// On the client (ASP.NET Core OIDC handler):\noptions.GetClaimsFromUserInfoEndpoint = true;\noptions.SaveTokens = true;\n```\n\n### AlwaysIncludeUserClaimsInIdToken\n\nSetting `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).\n\n```csharp\n// ⚠️ Use sparingly — increases id_token size significantly\nvar client = new Client\n{\n ClientId = \"native_app\",\n AlwaysIncludeUserClaimsInIdToken = true,\n AllowedScopes = { \"openid\", \"profile\", \"email\" },\n};\n```\n\n### Access Token\n\n- Purpose: authorizes API calls.\n- Audience: the resource server (API).\n- Contains: `sub`, `client_id`, `scope`, `jti`, `iss`, `exp`, + any user claims from profile service.\n- Resource-based filtering applies: claims associated with a specific `ApiResource` only appear when that resource is requested via resource indicator.\n\n### Resource-Based Claim Filtering\n\nDeclare claims on `ApiResource` to scope them to that specific API:\n\n```csharp\n// ✅ Claims on ApiResource are only emitted when that resource is requested\nnew ApiResource(\"invoicing\", \"Invoicing API\")\n{\n Scopes = { \"invoicing.read\", \"invoicing.write\" },\n UserClaims = { \"cost_center\", \"approval_limit\" } // Only in tokens for this API\n}\n\nnew ApiScope(\"invoicing.read\")\n{\n UserClaims = { \"department\" } // Emitted when this scope is requested\n}\n```\n\n### Claim Value Types\n\nSet `ClaimValueType` to ensure correct JSON serialization in the JWT:\n\n```csharp\n// ✅ Numeric and boolean claims serialize as JSON primitives\nvar claims = new List<Claim>\n{\n new(\"account_id\", \"42\",\n ClaimValueTypes.Integer64),\n\n new(\"is_verified\", \"true\",\n ClaimValueTypes.Boolean),\n\n new(\"permissions\", \"\"\"[\"read\",\"write\"]\"\"\",\n IdentityServerConstants.ClaimValueTypes.Json),\n};\n```\n\n```csharp\n// ❌ Without ClaimValueType, all values serialize as JSON strings\n// { \"account_id\": \"42\" } ← wrong, should be 42\nnew Claim(\"account_id\", \"42\")\n```\n\n---\n\n## Claim Types and Mapping\n\n### JwtClaimTypes vs. System ClaimTypes\n\nThe `Duende.IdentityModel` (or `IdentityModel`) library provides `JwtClaimTypes` with the short JWT/OIDC claim names:\n\n| JwtClaimTypes | Long Microsoft ClaimTypes |\n|---|---|\n| `sub` | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` |\n| `name` | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` |\n| `email` | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` |\n| `role` | `http://schemas.microsoft.com/ws/2008/06/identity/claims/role` |\n| `given_name` | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname` |\n\nAlways use `JwtClaimTypes` constants in IdentityServer code and in APIs that validate JWTs directly.\n\n### MapInboundClaims = false (Required in APIs)\n\nThe default JWT bearer handler maps short JWT claim names to long Microsoft WS-Federation names. Disable this:\n\n```csharp\n// ✅ In your API — keep standard OIDC short names\nbuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n .AddJwtBearer(options =>\n {\n options.Authority = \"https://identity.example.com\";\n options.Audience = \"my_api\";\n options.MapInboundClaims = false; // Keep \"sub\", not the long name\n });\n```\n\n```csharp\n// ✅ In web app OIDC handler — same principle\nbuilder.Services.AddAuthentication(...)\n .AddOpenIdConnect(\"oidc\", options =>\n {\n options.Authority = \"https://identity.example.com\";\n options.MapInboundClaims = false;\n options.TokenValidationParameters.NameClaimType = JwtClaimTypes.Name;\n options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role;\n });\n```\n\n```csharp\n// ❌ Without MapInboundClaims = false:\n// User.FindFirst(\"sub\") → null\n// User.FindFirst(\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier\") → found\n```\n\n---\n\n## IClaimsTransformation (API-Side)\n\n`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.\n\n### When to Use IClaimsTransformation\n\n- Enriching the `ClaimsPrincipal` with application-specific roles from a local database, after validating a token from IdentityServer.\n- Mapping external department/group memberships to application roles without putting that data in the token.\n- Adding denormalized claims (e.g. resolved tenant name from `tenant_id`) for use in authorization policies.\n\n```csharp\n// ✅ In an API project — augment principal after token validation\npublic sealed class TenantClaimsTransformation : IClaimsTransformation\n{\n private readonly ITenantRepository _tenants;\n\n public TenantClaimsTransformation(ITenantRepository tenants)\n {\n _tenants = tenants;\n }\n\n public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)\n {\n var tenantId = principal.FindFirstValue(\"tenant_id\");\n if (tenantId is null)\n {\n return principal;\n }\n\n var tenant = await _tenants.GetByIdAsync(tenantId);\n if (tenant is null)\n {\n return principal;\n }\n\n // Clone before mutating — ClaimsPrincipal can be reused across calls\n var identity = new ClaimsIdentity();\n identity.AddClaim(new Claim(\"tenant_name\", tenant.DisplayName));\n identity.AddClaim(new Claim(\"tenant_region\", tenant.Region));\n\n foreach (var role in tenant.ApplicationRoles)\n {\n identity.AddClaim(new Claim(ClaimTypes.Role, role));\n }\n\n principal.AddIdentity(identity);\n return principal;\n }\n}\n```\n\n```csharp\n// Program.cs — in the API\nbuilder.Services.AddTransient<IClaimsTransformation, TenantClaimsTransformation>();\n```\n\n> **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.\n\n---\n\n## Extension Grant Validators\n\n`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>()`.\n\n> See [docs/extension-grant-claims.md](docs/extension-grant-claims.md) for a full token exchange validator implementation with error handling and custom claim propagation.\n\n---\n\n## Claims from External Providers\n\nWhen 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.\n\n> See [docs/external-provider-claims.md](docs/external-provider-claims.md) for the full callback controller implementation with Google/AAD claim mapping and `ClaimActions` examples.\n\n---\n\n## Dynamic Claims Loading\n\nLoading 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.\n\n```csharp\npublic sealed class DynamicProfileService : DefaultProfileService\n{\n private readonly IUserPermissionService _permissions;\n private readonly IFeatureFlagService _features;\n private readonly ILogger<DynamicProfileService> _logger;\n\n public DynamicProfileService(\n IUserPermissionService permissions,\n IFeatureFlagService features,\n ILogger<DynamicProfileService> logger)\n : base(logger)\n {\n _permissions = permissions;\n _features = features;\n _logger = logger;\n }\n\n public override async Task GetProfileDataAsync(ProfileDataRequestContext context)\n {\n var subjectId = context.Subject.GetSubjectId();\n\n // Run database calls concurrently\n var (permissionsTask, featuresTask) = (\n _permissions.GetForUserAsync(subjectId, context.Client.ClientId),\n _features.GetEnabledForUserAsync(subjectId)\n );\n\n await Task.WhenAll(permissionsTask, featuresTask);\n\n var claims = new List<Claim>();\n\n // Role claims from permission service\n foreach (var permission in permissionsTask.Result)\n {\n claims.Add(new Claim(JwtClaimTypes.Role, permission));\n }\n\n // Feature flag claims — serialize as JSON array\n var featuresJson = System.Text.Json.JsonSerializer.Serialize(featuresTask.Result);\n claims.Add(new Claim(\n \"features\",\n featuresJson,\n IdentityServerConstants.ClaimValueTypes.Json));\n\n context.AddRequestedClaims(claims);\n }\n\n public override async Task IsActiveAsync(IsActiveContext context)\n {\n // Guard against token use after account suspension\n var subjectId = context.Subject.GetSubjectId();\n context.IsActive = await _permissions.IsUserActiveAsync(subjectId);\n }\n}\n```\n\n> **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.\n\n### Caching Dynamic Claims\n\n```csharp\npublic override async Task GetProfileDataAsync(ProfileDataRequestContext context)\n{\n var subjectId = context.Subject.GetSubjectId();\n var cacheKey = $\"profile:{subjectId}:{context.Client.ClientId}\";\n\n if (!_cache.TryGetValue(cacheKey, out IReadOnlyList<Claim>? cachedClaims))\n {\n cachedClaims = await LoadClaimsFromDatabaseAsync(subjectId, context.Client.ClientId);\n\n _cache.Set(cacheKey, cachedClaims, TimeSpan.FromMinutes(5));\n }\n\n context.AddRequestedClaims(cachedClaims!);\n}\n```\n\n---\n\n## Client Claims\n\nClient 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.\n\n```csharp\nvar client = new Client\n{\n ClientId = \"billing-service\",\n ClientSecrets = { new Secret(\"secret\".Sha256()) },\n AllowedGrantTypes = GrantTypes.ClientCredentials,\n AllowedScopes = { \"invoicing.api\" },\n\n // Prefixed as \"client_customer_id\" in the token\n Claims =\n {\n new ClientClaim(\"customer_id\", \"acme-corp\"),\n new ClientClaim(\"region\", \"us-east\"),\n },\n\n // Remove the prefix: emit as \"customer_id\" (use carefully)\n // ClientClaimsPrefix = \"\"\n};\n```\n\n> Client claims are only emitted in the **client credentials flow** by default. For other flows set `AlwaysSendClientClaims = true` on the client definition.\n\nFor dynamic client claims (e.g. set based on runtime context), implement a custom token request validator:\n\n```csharp\npublic sealed class DynamicClientClaimsValidator : ICustomTokenRequestValidator\n{\n private readonly IClientContextService _clientContext;\n\n public DynamicClientClaimsValidator(IClientContextService clientContext)\n {\n _clientContext = clientContext;\n }\n\n public async Task ValidateAsync(CustomTokenRequestValidationContext context)\n {\n if (context.Result.ValidatedRequest.GrantType != GrantType.ClientCredentials)\n {\n return;\n }\n\n var clientId = context.Result.ValidatedRequest.Client.ClientId;\n var tier = await _clientContext.GetSubscriptionTierAsync(clientId);\n\n context.Result.ValidatedRequest.ClientClaims.Add(\n new Claim(\"subscription_tier\", tier));\n }\n}\n```\n\n---\n\n## Common Pitfalls\n\n### Claims Not Appearing in Tokens\n\n1. **Claim not in `UserClaims`**: The claim type must be listed in the `UserClaims` collection of the `IdentityResource`, `ApiScope`, or `ApiResource` that the client requests.\n\n```csharp\n// ❌ \"department\" never requested — won't appear even if profile service emits it\nnew ApiScope(\"api.read\"); // no UserClaims\n\n// ✅ Declare the claim on the scope\nnew ApiScope(\"api.read\")\n{\n UserClaims = { \"department\", \"cost_center\" }\n}\n```\n\n2. **Client not requesting the scope**: The client must include the scope in `AllowedScopes` and request it at authorization time.\n\n3. **`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.\n\n4. **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.\n\n5. **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.\n\nAdding 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.\n\n### Wrong Claim Names in APIs\n\nCaused by not setting `MapInboundClaims = false`. The JWT bearer handler renames `sub` to the long WS-Federation URI. Fix:\n\n```csharp\n// ✅ Always set this in APIs consuming IdentityServer tokens\noptions.MapInboundClaims = false;\n```\n\n### Mutating ClaimsPrincipal in IClaimsTransformation\n\n`ClaimsPrincipal` instances can be cached and reused. Always create a new `ClaimsIdentity` and add it to the principal rather than mutating an existing identity:\n\n```csharp\n// ✅ Create a new identity, add to existing principal\nvar identity = new ClaimsIdentity();\nidentity.AddClaim(new Claim(\"app_role\", \"admin\"));\nprincipal.AddIdentity(identity);\nreturn principal;\n\n// ❌ Never mutate the principal's existing identities in-place\n((ClaimsIdentity)principal.Identity!).AddClaim(new Claim(\"app_role\", \"admin\"));\n```\n\n### AlwaysIncludeUserClaimsInIdToken Overuse\n\nSetting `AlwaysIncludeUserClaimsInIdToken = true` embeds all profile claims in the identity token. This:\n- Increases token size (can exceed header/cookie limits).\n- Caches profile data in the client until the token expires (stale claims).\n- Bypasses the userinfo endpoint's on-demand freshness.\n\nPrefer `options.GetClaimsFromUserInfoEndpoint = true` in the client OIDC handler.\n\n### Storing Too Many Claims in the Session Cookie\n\nThe 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.\n\n### Forgetting IsActiveAsync\n\n`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.\n\n---\n\n## Resources\n\n- [Duende IdentityServer — Claims fundamentals](https://docs.duendesoftware.com/identityserver/fundamentals/claims/)\n- [Duende IdentityServer — Profile Service reference](https://docs.duendesoftware.com/identityserver/reference/services/profile-service/)\n- [Duende IdentityServer — Identity Resources](https://docs.duendesoftware.com/identityserver/fundamentals/resources/identity/)\n- [Duende IdentityServer — API Scopes](https://docs.duendesoftware.com/identityserver/fundamentals/resources/api-scopes/)\n- [Duende IdentityServer — API Resources](https://docs.duendesoftware.com/identityserver/fundamentals/resources/api-resources/)\n- [Duende IdentityServer — Extension Grants](https://docs.duendesoftware.com/identityserver/tokens/extension-grants/)\n- [Duende IdentityServer — External Providers](https://docs.duendesoftware.com/identityserver/ui/login/external/)\n- [Duende IdentityServer — Token types overview](https://docs.duendesoftware.com/identityserver/tokens/)\n- [Duende IdentityServer — Custom Token Request Validator](https://docs.duendesoftware.com/identityserver/tokens/dynamic-validation/)\n- [ASP.NET Core — IClaimsTransformation](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims)\n- [OpenID Connect Core spec — Standard scope/claim mappings](https://openid.net/specs/openid-connect-core-1_0.html#scopeclaims)\n\n### Related Skills\n\n- `aspnetcore-authorization` — policy-based authorization, `IAuthorizationRequirement`, resource-based authorization using claims in the `ClaimsPrincipal`\n- `identityserver-configuration` — configuring `IdentityResource`, `ApiScope`, `ApiResource`, and `Client` definitions that drive which claims are requested\n- `aspnetcore-authentication` — cookie authentication, OIDC handler configuration, `MapInboundClaims`, and `GetClaimsFromUserInfoEndpoint`\n"
}SHA-256: 46da9df18a2a9383f517bae48d6301c4e01826650da7589d6c24e8c812ddbd6b