Claude
Skills
Sign in
Back

claims-authorization

Included with Lifetime
$97 forever

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

General

What this skill does


# 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/tokens/authorization

---

## Sub-Documents

| Document | Description | When to Load |
|----------|-------------|--------------|
| [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 |
| [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 |

---

## 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.Services
public 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

| Member | Description |
|---|---|
| `Subject` | The `ClaimsPrincipal` from the authentication session (or from the access token for userinfo calls). |
| `Client` | The `Client` making the request — use for per-client filtering. |
| `Caller` | What triggered this call: `ClaimsProviderAccessToken`, `ClaimsProviderIdentityToken`, `UserInfoEndpoint`. |
| `RequestedClaimTypes` | Claim types requested by the client via scopes/resources. |
| `IssuedClaims` | Populate 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 AddRequestedClaims
public 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),
    

Related in General