Introducing the next era of Duende IdentityServer.

Read our CEO’s announcement

How Duende IdentityServer Filters Claims (And Why It Matters)

Two blue circles
Summary: Duende IdentityServer intentionally filters claims that appear in a user’s principal but are missing from the issued token. This design enforces the principle of least privilege, ensuring tokens contain only what was explicitly requested and helping keep token size manageable. The filtering occurs in AddRequestedClaims, which checks if a claim’s type is present in RequestedClaimTypes. This list is derived from the UserClaims defined on the IdentityResource objects associated with the scopes requested by the client. To ensure a claim, such as department, appears in the token, you must configure an IdentityResource to include that claim type and ensure the client requests the corresponding scope. Configuring resources is the secure and recommended approach rather than implementing a custom IProfileService to bypass the filter.

The other day, Anders Abel, founder of Sustainsys, and I were chatting about the parts of Duende IdentityServer that quietly do the right thing without anyone noticing. The conversation started from a recent post about ASP.NET Core cookie size limits, specifically Fix #7, which recommends using IdentityServer’s configuration system to control which claims end up in tokens.

Anders pointed out that the reason Fix #7 works so cleanly is a method most developers never look at directly: AddRequestedClaims. It trips up almost every developer at some point, and it’s easy to miss precisely because it works so well. Until you’re staring at a decoded token, wondering where your claim went.

If you’ve ever added a department or role claim to your user principal and then watched it vanish from the id_token, this post is for you.

“But the Claim Is Right There!”

Say you’ve added a custom claim (department, say) to your user during sign-in. You can see it on HttpContext.User.Claims. You’ve confirmed it’s on the principal. You request a token, decode it at jwt.me, and... it’s not there.

The natural assumption is that something is broken. The claim exists, so IdentityServer should pass it through. It doesn’t, and that’s intentional. The claim isn’t missing. It’s being filtered out, on purpose.

Inside DefaultProfileService

When IdentityServer needs to populate a token with user claims, it calls IProfileService.GetProfileDataAsync. If you haven’t registered a custom implementation, it uses DefaultProfileService, which ships with IdentityServer and looks like this:

Csharp

public virtual Task GetProfileDataAsync(ProfileDataRequestContext context, CancellationToken _)
{
    // ...
    context.LogProfileRequest(Logger);
    context.AddRequestedClaims(context.Subject.Claims);
    context.LogIssuedClaims(Logger);

    return Task.CompletedTask;
}
  • context.Subject.Claims — these are all the claims on the authenticated user’s principal. Everything.
  • But they’re passed through AddRequestedClaims rather than added directly to the token.
  • The logging calls are worth noting as well: LogProfileRequest logs, which claim types were requested, and LogIssuedClaims logs what was actually issued. If you turn on Debug-level logging, you can watch the filter in action.

AddRequestedClaims is where the filtering happens.

Important note: while Duende IdentityServer ships with DefaultProfileService, it is typically replaced in production scenarios with either Duende’s ASP.NET Core Identity implementation or a custom IProfileService written by the implementing team.

The Gatekeeper: AddRequestedClaims

AddRequestedClaims delegates to FilterClaims, and both are short enough to read in full:

Csharp

public static List<Claim> FilterClaims(this ProfileDataRequestContext context, IEnumerable<Claim> claims)
{
    ArgumentNullException.ThrowIfNull(context);
    ArgumentNullException.ThrowIfNull(claims);

    return claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
}

public static void AddRequestedClaims(this ProfileDataRequestContext context, IEnumerable<Claim> claims)
{
    if (context.RequestedClaimTypes.Any())
    {
        context.IssuedClaims.AddRange(context.FilterClaims(claims));
    }
}

FilterClaims keeps only claims whose Type appears in context.RequestedClaimTypes. Everything else is dropped. AddRequestedClaims adds one more guard: if RequestedClaimTypes is empty (meaning nothing was requested), nothing gets through.

So your department claim isn’t missing from the token. It’s being filtered because "department" was never in RequestedClaimTypes. The filter is working as designed.

So Who Decides What’s “Requested”?

RequestedClaimTypes is populated by the claims pipeline before IProfileService is ever called. The source is your Identity Resource configuration, specifically the UserClaims property on each IdentityResource.

The chain works like this:

  1. A client requests scopes — for example, openid profile department_info
  2. IdentityServer resolves those scope names to IdentityResource objects
  3. Each IdentityResource has a UserClaims collection listing which claim types it covers
  4. All those claim types get flattened into RequestedClaimTypes

Internally, for the userinfo endpoint path, that flattening looks like this (from UserInfoResponseGenerator):

Csharp

var identityResources = resourceValidationResult.Resources.IdentityResources;
result = identityResources.SelectMany(x => x.UserClaims).Distinct();

That single line is the bridge between your scope configuration and the claims filter. If your claim type isn’t listed in any requested IdentityResource.UserClaims, it won’t appear in RequestedClaimTypes, and FilterClaims will drop it.

The mental model: scopes → identity resources → UserClaims → RequestedClaimTypes → FilterClaims → IssuedClaims.

graph LR
    A[Scopes] --> B[Identity Resources]
    B --> C[UserClaims]
    C --> D[RequestedClaimTypes]
    D --> E[FilterClaims]
    E --> F[IssuedClaims]

A note on id_token vs. the userinfo endpoint: When a client requests both an identity scope and an access token in the same request, IdentityServer suppresses identity claims from the id_token by default. They’re available via the userinfo endpoint instead. If you’re not seeing your claim in the id_token even after configuring your resources correctly, this is likely why. You can override this per-client with AlwaysIncludeUserClaimsInIdToken = true, or simply fetch the claims from the userinfo endpoint, which is the recommended approach. The AlwaysIncludeUserClaimsInIdToken is available for backward compatibility and is intended only for use with clients that do not support calls to the userinfo endpoint.

The Right Fix: Configure Your Resources

Now that you understand the pipeline, the fix is straightforward. If you want department in the token, declare an IdentityResource that includes it:

Csharp

new IdentityResource
{
    Name = "department_info",
    DisplayName = "Department Information",
    UserClaims = { "department" }
}

Then make sure the client requests the department_info scope. Once it does, department will appear in RequestedClaimTypes, pass through FilterClaims, and land in the token.

You’re telling IdentityServer: when a client asks for department_info, they want the department claim. The filtering follows from that configuration.

The Escape Hatch: Custom IProfileService

Sometimes you need more control. Maybe you’re prototyping, or you have a claim that should always be present regardless of what scopes were requested. In that case, you can implement IProfileService directly:

Csharp

public class CustomProfileService : IProfileService
{
    public Task GetProfileDataAsync(ProfileDataRequestContext context, CancellationToken ct)
    {
        // Still respect the filter for most claims
        context.AddRequestedClaims(context.Subject.Claims);

        // But always include department, regardless of what was requested
        var department = context.Subject.FindFirst("department");
        if (department != null)
        {
            context.IssuedClaims.Add(department);
        }

        return Task.CompletedTask;
    }

    public Task IsActiveAsync(IsActiveContext context, CancellationToken ct)
    {
        context.IsActive = true;
        return Task.CompletedTask;
    }
}

Register it in your DI container:

Csharp

builder.Services.AddIdentityServer()
    .AddProfileService<CustomProfileService>();

This example still calls AddRequestedClaims for the bulk of the claims, and only bypasses the filter for the specific claim it needs to force through. That’s a good pattern: use the escape hatch surgically, not as a wholesale replacement for the filtering behavior.

Adding claims directly to IssuedClaims without filtering means you’re responsible for not leaking sensitive data to clients that shouldn’t see it. The filter exists for a reason.

Security by Default

That reason is “least privilege for claims.” Tokens should only contain what was explicitly requested and configured. Without this filter, every claim on the user principal would appear in every token, including claims meant for internal use, sensitive attributes, or data irrelevant to the requesting client.

This is the same philosophy as scope-based access control: being authenticated doesn’t mean you get everything. It means you get what you asked for, and only what you asked for.

There’s a practical benefit too: smaller tokens. Tokens that travel in cookies or HTTP headers are subject to size constraints. Filtering out unrequested claims keeps tokens lean. As the cookie size limits post covers, this matters more than you’d think in production.

Hidden in Plain Sight

This is exactly the kind of thing Anders was talking about: a design decision that quietly protects you, but looks like a bug the first time you hit it.

Before you reach for a custom IProfileService, check your IdentityResource configuration. The claim you’re looking for is probably there on the principal, waiting to be requested. Add it to the right resource’s UserClaims, make sure the client requests that scope, and the filter will do the rest.

Related Articles