If you have spent any time writing ASP.NET Core security code, you have probably written something like this more than once:
var sub = User.FindFirst("sub")?.Value;
var isAdmin = User.HasClaim("role", "admin");
var scopes = User.FindFirst("scope")?.Value?.Split(' ') ?? [];
var hasInvoiceRead = scopes.Contains("invoice.read");
Nothing is technically wrong here, but the problems add up. The claim type strings "sub", ClaimTypes.Email, and "scope" are scattered across controllers, middleware, and authorization handlers. Every call site has to remember to null-check. When a claim name changes, you search and replace across the whole project and hope you caught everything. The code reads like plumbing rather than intent.
C# 14, shipping with .NET 10, introduces extension members: a language feature that lets you attach properties (not just methods) to existing types. Applied to ClaimsPrincipal, it gives you a clean, centralized place to put all of that claim-access logic, with zero runtime overhead.
A Brief History of .NET Security
To understand why ClaimsPrincipal looks the way it does, it helps to know its origins.
The original .NET security model, dating back to .NET Framework 1.0, was built around WindowsPrincipal and WindowsIdentity. If you have been writing code using the .NET Framework long enough, you will remember code like this:
if (User.IsInRole("DOMAIN\\Administrators"))
{
// allow
}
This worked well for intranet applications where every user was an Active Directory account and every permission was mapped to a Windows group. The model was simple and effective for that world. The problem was that Active Directory accounts were tightly coupled to Windows: there was no portable way to represent user attributes like email addresses, no way to express fine-grained permissions like "read access to invoices," and no concept of tokens issued by an external authority. If you needed anything beyond group membership, you were on your own.
Microsoft introduced Windows Identity Foundation (WIF) around 2009 to bring claims-based identity to the .NET platform. The core shift was from "who are you in this OS?" to "what has a trusted authority asserted about you?" Rather than checking group membership directly, your code inspects a collection of claims: name-value pairs issued by an identity provider.
.NET Framework 4.5 folded WIF's key types straight into the base class library, making ClaimsPrincipal and ClaimsIdentity the standard representation of a user. ASP.NET Core continued this direction: regardless of which authentication scheme is used (cookies, JWT bearer tokens, Windows auth, or anything else), the User object is always a ClaimsPrincipal.
A ClaimsPrincipal can hold multiple ClaimsIdentity instances. For example, a user authenticated with both a session cookie and a certificate will have two identities under a single principal. Each ClaimsIdentity holds a list of Claim objects.
The API looks like this in practice:
// Get a single claim value
var email = User.FindFirst(ClaimTypes.Email)?.Value;
// Check for a specific claim
var hasScope = User.HasClaim("scope", "invoice.read");
// Get all values for a claim type
var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();
This API is functional, but low-level. Every call site must know the exact claim-type string, whereas null propagation requires manual effort. The same boilerplate appears in every controller, every policy handler, every middleware that touches the user identity. There is no good place to consolidate this without resorting to static helper methods or classic extension methods that can only surface as method calls, never as properties.
That last limitation is exactly what C# 14 solves!
C# 14 Extension Members
C# 3.0 gave us extension methods via the this parameter convention:
public static class ClaimsPrincipalExtensions
{
public static string? GetEmail(this ClaimsPrincipal principal) =>
principal.FindFirst(ClaimTypes.Email)?.Value;
}
While useful, this is limited to only adding methods. There was no way to write User.Email as a property. You could write User.GetEmail(), but that is not how properties work, nor how developers expect to access simple values on an object.
C# 14 introduces extension blocks. You declare the receiver type once, then define any number of instance members (both properties and methods) inside the block:
public static class SomeExtensions
{
extension(ReceiverType receiver)
{
public string SomeProperty => receiver.InternalValue;
public void SomeMethod(string arg) { /* ... */ }
}
}
The members behave as if they were instance members of ReceiverType. You call them with dot notation, like any other property or method. Under the hood, the compiler generates the necessary IL to translate the extension property to a new method bound to the existing type, so there is no runtime cost. Existing code continues to compile unchanged; this is purely additive.
Let’s see how we can use extension properties in a real-world example.
Before: Accessing Claims Manually
In a typical controller action, claim access today looks like this:
[HttpGet("{id}")]
public IActionResult GetInvoice(int id)
{
var sub = User.FindFirst("sub")?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
if (sub is null || email is null)
return Forbid();
if (!User.HasClaim("scope", "invoice.read"))
return Forbid();
// business logic...
return Ok();
}
The same pattern appears in every authorization handler, every API endpoint, and every middleware that touches the user identity. The strings "sub" and "scope" are implicit contracts between your application and your identity provider. If either side changes them, the compiler cannot help you find all the call sites that need updating.
Of course, you can use a static class containing claim name constants to at least solve that problem, but you still need to keep nullability in mind, or deal with more complex claim types over and over again.
After: Extension Properties on ClaimsPrincipal
With a C# 14 extension block, you move all of that logic into one file:
// ClaimsPrincipalExtensions.cs
public static class ClaimsPrincipalExtensions
{
extension(ClaimsPrincipal principal)
{
public string? SubjectId => principal.FindFirst("sub")?.Value;
public string? Email => principal.FindFirst(ClaimTypes.Email)?.Value;
public bool HasScope(string scope) =>
principal.HasClaim("scope", scope);
}
}
The controller action becomes:
[HttpGet("{id}")]
public IActionResult GetInvoice(int id)
{
if (User.SubjectId is null || User.Email is null)
return Forbid();
if (!User.HasScope("invoice.read"))
return Forbid();
// business logic...
return Ok();
}
SubjectId and Email are properties because they take no arguments and represent a single value on the principal. HasScope is a method because it takes a parameter. The extension block handles both forms naturally in the same declaration.
Notice how the call site now reads like domain code rather than claim-lookup noise. You are not thinking about FindFirst or null propagation; you are thinking about subjects, emails, and scopes.
All of the magic strings live in one file: ClaimsPrincipalExtensions.cs. A security review of how your application reads user identity starts and ends there. Adding a new claim type means adding one property, and every consumer benefits immediately. If your identity provider renames a claim, you fix it in one place.
Note: extension properties can also contain a setter. In most cases, you won’t have access to the internal state of the class you’re extending, but you could use this to create strongly-typed properties over a dictionary. Take, for example, the
AuthenticationPropertiesclass:
public static class AuthenticationPropertiesExtensions
{
extension(AuthenticationProperties properties)
{
public int? Attempts
{
get
{
if (properties.Items.TryGetValue("attempts", out var value)
&& int.TryParse(value, out var attempts))
{
return attempts;
}
return null;
}
set => properties.Items["attempts"] = value?.ToString();
}
}
}
// Example usage:
var signInProps = new AuthenticationProperties();
signInProps.Attempts = 1;
Summary
Extension members are a small, purely additive feature of the language. They produce the same IL as classic extension methods, integrate with existing tooling, and require no changes to your runtime or framework dependencies. You can adopt them incrementally, one property at a time.
For security-focused code, the payoff is disproportionately large relative to the effort. Claim access logic that was previously duplicated across every layer of your application can live in a single, auditable location. The magic strings that represent the contract between your application and your identity provider no longer have to be scattered across dozens of files.
If you are running Duende IdentityServer, the claims in your tokens (sub, email, custom scopes on your ApiResource configuration, resource indicators per RFC 8707) are exactly the kind of values this pattern is designed for. A well-named extension property on ClaimsPrincipal becomes a direct, readable mapping from the protocol to your application code. Pair it with resource isolation for scoped-down tokens, and you get a codebase where both the token contents and the code that reads them are tightly controlled.
Have questions or want to share how you use extension members in your security code? Join the conversation on GitHub Discussions.
Thanks for stopping by!
We hope this post helped you on your identity and security journey. If you need a hand with implementation, our docs are always open. For everything else, come hang out with the team and other developers on GitHub.
If you want to get early access to new features and products while collaborating with experts in security and identity standards, join us in our Duende Product Insiders program. And if you prefer your tech content in video form, our YouTube channel is the place to be. Don't forget to like and subscribe!
Questions? Comments? Just want to say hi? Leave a comment below and let's start a conversation.