惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
博客园 - 司徒正美
大猫的无限游戏
大猫的无限游戏
Last Week in AI
Last Week in AI
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
小众软件
小众软件
宝玉的分享
宝玉的分享
Apple Machine Learning Research
Apple Machine Learning Research
美团技术团队
WordPress大学
WordPress大学
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
罗磊的独立博客
The Cloudflare Blog
V
V2EX
月光博客
月光博客
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
博客园 - 【当耐特】
T
Tailwind CSS Blog

Duende Software Official Site

The Backend for Frontend Pattern Is Now Official IETF Guidance: RFC 10017 Published WhatsApp One-Time Password (OTP) Login with Duende IdentityServer and User Management Planning a Successful Migration from IdentityServer3 to Duende IdentityServer Client Secrets, Mutual TLS and Private Key JWT, Oh My! How To Spell "Duende" Understanding .NET 11 Automatic CSRF Protection: A Guide for Identity Developers Security Lingo Explained: TOTP (Time-based One-Time Password) Custom Passkey Attestation Policies: Restricting Login to Hardware Keys OAuth Identity Chaining, Transaction Tokens, and Human-in-the-Loop: Summer 2026 Identity Standards Recap What is Identity? - The Question Every Team Should Answer Before Writing Code Security Is a Spectrum: How to Choose Session Lifetimes in Duende IdentityServer Passkeys and WebAuthn with Duende IdentityServer and User Management Authenticating Players in Godot 4 with OAuth 2.0 and OpenID Connect Hardening OAuth in the newest 2026-07-28 MCP Release Candidate Unify Your SAML and OIDC Signing Keys with Automatic Rotation and Duende IdentityServer Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Stop AI Bots from Wasting Your Server Core vs Extended Protocols in Duende IdentityServer v8: What You Get and When You Need More Your IdentityServer v8 Upgrade Checklist: A Quick Pre-Flight Guide Setting Up SAML Single Sign-On in ASP.NET with Duende IdentityServer Your Identity, Your Terms: Duende's Modular Identity Infrastructure and v8.x Release Duende Spring Launch '26: Identity Infrastructure That Expands With You SAML and OpenID Connect (OIDC): Coexistence, Not Competition
How Duende IdentityServer Filters Claims (And Why It Matt...
Khalid Abuhakmeh · 2026-06-18 · via Duende Software Official Site
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.