developers

.NET 10: What’s New for Authentication and Authorization

Dive into the latest .NET 10 updates for authentication and authorization, and important breaking changes for .NET developers.

With every release, the .NET platform evolves to meet the increasingly complex demands of modern application security. The release of .NET 10 is no exception, bringing a host of powerful new features and refinements to the authentication and authorization stack. These updates focus on phishing-resistant credentials, improved developer experience, enhanced observability, and clearer security boundaries.

In this article, we’ll explore the most impactful authentication and authorization updates in the latest release of .NET.

Passkey Support in ASP.NET Core Identity

Passkeys are widely seen as the next step in authentication, offering a phishing-resistant and user-friendly alternative to passwords. They are passwordless credentials backed by the FIDO2 and WebAuthn standards.

learnpasskeys.io logo

Curious about how passkeys work? Try passkeys now →

learnpasskeys.io

ASP.NET Core Identity in .NET 10 includes built-in support for passkey registration and authentication, offering seamless integration with the existing Identity infrastructure. This feature is designed to support the most common WebAuthn scenarios and is included by default in the Blazor Web App project template, requiring only developer configuration.

The implementation is scoped to authentication scenarios and is not intended as a general-purpose WebAuthn library.

Developers can configure passkey behavior using the IdentityPasskeyOptions class. This allows setting options like the ServerDomain (Relying Party ID), AuthenticatorTimeout, and ChallengeSize.

Here is a basic configuration example:

// Program.cs
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com";
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.ChallengeSize = 64;
});

The current implementation has some specific limitations:

  • It does not validate attestation statements by default, so you cannot be certain of the authenticity of the authenticator device.
  • Passkeys are treated as a primary authentication factor, not as a second factor for 2FA.
  • Template support is currently limited to the Blazor Web App template.

For more information, check out the official documentation.

Streamlining Claims with C# 14 Extension Members

One of the most persistent annoyances for .NET developers has been accessing user claims. For years, codebases have been littered with repetitive, brittle, and hard-to-read logic just to get a user's ID or email.

The traditional approach relies on LINQ and "magic strings" to query the ClaimsPrincipal object, as the following code shows:

// The "old" way, repeated in controllers and services
var userIdString = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
var userId = Guid.Parse(userIdString ?? throw new Exception("Invalid User ID"));

var email = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;

This pattern is problematic for several reasons: it’s messy and repetitive; it’s not easy to test; a typo in the ClaimTypes string won't be caught until runtime; there is no IntelliSense support.

C# 14, supported in .NET 10, introduces extension members. This new language feature allows you to define properties and methods on existing types (like ClaimsPrincipal) from within a static class. It's an evolution of extension methods that offers cleaner syntax and better IDE integration.

Here is how you can refactor your claim access logic into a clean, reusable extension:

using System.Security.Claims;

public static class ClaimsPrincipalExtensions
{
    // Use the 'extension' keyword on the 'this' parameter
    extension (ClaimsPrincipal principal)
    {
        // Define a computed property
        public string Email => principal.Claims
            .FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;

        // Define a method with parsing logic
        public Guid GetUserId() => Guid.TryParse(
            principal.Claims.FirstOrDefault(
                c => c.Type == ClaimTypes.NameIdentifier)?.Value,
            out var userId) ? userId : throw new Exception("Invalid User ID");
    }
}

Now, your application code becomes incredibly clean, readable, and testable:

var userId = User.GetUserId();
var email = User.Email;

This approach centralizes your claims logic, eliminates magic strings from your controllers, and makes your code self-documenting. The new extension properties and methods show up in IntelliSense just like native members, dramatically improving discoverability and the overall developer experience.

New Authentication and Authorization Metrics

.NET 10 introduces new built-in authentication and authorization metrics, providing deep insights into your application's security posture and performance. These metrics are exposed via the System.Diagnostics.Metrics API and integrate seamlessly with tools like OpenTelemetry, Prometheus, and Azure Monitor.

Core Authentication Metrics

New metrics are available for some authentication and authorization events, such as:

  • aspnetcore.authentication.sign_ins: A counter that increments every time an authentication handler successfully validates a principal.
  • aspnetcore.authentication.sign_outs: A counter that increments when a logout event occurs.
  • aspnetcore.authentication.authenticate.duration: A histogram that records the time (in seconds) it takes for each authentication handler to run. This helps diagnose performance bottlenecks in your auth pipeline (e.g., slow database calls or external identity provider lookups).

ASP.NET Core Identity Metrics

In addition to the generic authentication and authorization metrics, ASP.NET Core Identity gets its own dedicated metrics that tracks key user lifecycle events. These metrics include:

  • aspnetcore.identity.user.create.duration: Duration of user creation activity.
  • aspnetcore.identity.sign_in.sign_ins: A counter of successful authentications.
  • aspnetcore.identity.sign_in.sign_outs: A counter of successful logouts.
  • aspnetcore.identity.sign_in.check_password_attempts: A counter of the number of password check attempts at login.
  • aspnetcore.identity.sign_in.two_factor_clients_forgotten: A counter of forgotten 2FA operations during the login.

These counters, often tagged by failure reason, provide a clear, high-level dashboard of your user-facing security operations. You can easily set up alerts for high failure rates on login or registration, helping you spot issues in real-time.

For more information, read the official documentation for the ASPNET Core metrics overview and the ASP.NET Core built-in metrics.

A Key Breaking Change: Unauthorized Access to API Endpoints

.NET 10 introduces a subtle but important breaking change in how cookie-based authentication handles unauthorized requests for API endpoints.

Previously, the cookie authentication handler had special logic. Unless the client calls the endpoint through XMLHttpRequests, any unauthenticated request would return a 302 Redirect to the configured login page instead of a 401 Unauthorized status code.

This was an annoying problem that forced developers to use workarounds.

In .NET 10, this special logic has been removed. By default, when an unauthenticated or unauthorized request is made to a known API endpoint, the handler will no longer issue a redirect. Instead, it will automatically return the correct status code that API clients expect:

  • 401 Unauthorized for unauthenticated requests.
  • 403 Forbidden for unauthorized (forbidden) requests.

This new default behavior greatly simplifies the code for modern applications, removing the need for custom event handlers and making the framework do the "right thing" out of the box. The framework identifies "known API endpoints" (such as those marked with [ApiController], minimal APIs, or those implementing the new IApiEndpointMetadata interface) and applies this new smarter logic.

To learn the difference between 401 and 403 status code, read this blog post.

If your application, for any reason, depended on the old behavior of redirecting API endpoints to a login page, you can opt-out and restore the previous behavior.

You can do this by explicitly handling the redirect events and forcing the redirect to occur, just as the old behavior did:

// Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        // This explicitly restores the old "always redirect" behavior
        options.Events.OnRedirectToLogin = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };

        options.Events.OnRedirectToAccessDenied = context =>
        {
            context.Response.Redirect(context.RedirectUri);
            return Task.CompletedTask;
        };
    });

Read the official documentation for more details.

Improved Blazor OIDC Docs and Samples

Securing Blazor Web Apps, especially in "Auto" or "Server" render modes, with OpenID Connect (OIDC) has been a common point of confusion for developers. Handling the initial sign-in, token management, and calling a protected API require careful configuration.

In response to community feedback, the .NET 10 release includes a comprehensive update to the Blazor security documentation and samples.

While not a new framework feature, this is a critical enhancement for developer productivity and application security. The new documentation provides end-to-end guidance and best-practice samples for:

  • Integrating Blazor Web Apps with OIDC providers like Entra ID, Auth0, or Okta.
  • Correctly managing tokens and handling token refresh.
  • Propagating the user's identity from the interactive component to server-side services.

This update fills a gap that has caused quite a few problems for Blazor developers.

Auth0 has already addressed this gap some time ago with several blog posts:

A Significant Upgrade

.NET 10 delivers a significant upgrade to the ASP.NET Core security landscape. It tackles modern challenges with built-in passkey support, smooths over long-standing developer frustrations with C# 14 extension members for claims, and provides critical visibility with new metrics.

These updates demonstrate a clear focus on making secure-by-default applications easier to build, maintain, and monitor. By embracing phishing-resistant credentials, improving observability, and refining the developer experience, .NET 10 empowers developers to build the next generation of secure and robust applications.

What .NET 10 authentication feature are you most excited to implement in your projects?