---
title: "Authentication and Authorization: What's New in .NET 11"
description: "Learn what’s new in .NET 11 regarding authentication and authorization."
authors:
  - name: "Andrea Chiarelli"
    url: "https://auth0.com/blog/authors/andrea-chiarelli/"
date: "Nov 9, 2026"
category: "Developers"
tags: ["dotnet", "authentication", "authorization", "signalr"]
url: "https://auth0.com/blog/authentication-and-authorization-what-s-new-in-net-11/"
---

# Authentication and Authorization: What's New in .NET 11

When it comes to authentication and authorization, .NET 11 focuses on valuable refinements. It addresses key edge cases to enhance security and user experience: a WebSocket that outlives its access token, an authorization attribute that works on one endpoint type but not on another, a passkey list full of identical "Passkey" labels. Let's look at what the newest release brings to authentication and authorization.

## Refresh Authentication on Live SignalR Connections

A [SignalR](https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-11.0) connection can live for hours, while an access token usually doesn't. Until now, you had two unattractive options when the token expired: close the connection and make the client reconnect (missing messages in the gap), or leave it open and keep trusting an identity that is no longer valid.

.NET 11 adds a third option: **authentication refresh**. The client sends the old token over a dedicated `/refresh` endpoint, and the server swaps the connection's `ClaimsPrincipal` in place.

You apply authentication refresh when you map your SignalR hub, as shown below:

```csharp
// Program.cs
app.MapHub<ChatHub>("/chat", options =>
{
    options.CloseOnAuthenticationExpiration = true;
    options.EnableAuthenticationRefresh = true;
});
```

Setting `EnableAuthenticationRefresh` adds `/chat/refresh` next to the existing `/negotiate` route. If you pair it with `CloseOnAuthenticationExpiration`, then a connection that fails to refresh in time is closed instead of lingering with stale credentials.

If you run a reverse proxy or a web application firewall, remember to allow the new route. A proxy that forwards `/negotiate` but blocks `/refresh` will make every refresh fail.

The server doesn't blindly accept whatever token the client sends. By default, it compares the `sub` claim (falling back to `ClaimTypes.NameIdentifier` or `ClaimTypes.Upn`) of the old and new principals. A refresh that changes the subject is rejected with a 403 status code, so a connection can't quietly turn into a different user.

You can add your own checks with `OnAuthenticationRefresh`. It can reject a refresh, but it can't approve one that fails the built-in check:

```csharp
options.OnAuthenticationRefresh = context =>
{
    // Keep the connection inside the tenant it started in
    var sameTenant = context.NewUser.HasClaim("tenant", "contoso");
    return Task.FromResult(sameTenant);
};
```

On the client side, the .NET client refreshes automatically by default. It asks the server how much lifetime the token has left and schedules a refresh ahead of expiration (five minutes before, unless you change it). Each refresh calls your `AccessTokenProvider` again, so the provider is where you fetch a fresh token:

```csharp
var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat", options =>
    {
        options.AccessTokenProvider = GetAccessTokenAsync;
    })
    .WithAuthenticationRefresh(options =>
    {
        options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2);
    })
    .Build();
```

If you use Auth0, `GetAccessTokenAsync` is where you'd return a cached [access token](https://auth0.com/docs/secure/tokens/access-tokens) or use a [refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens) to get a new one. The JavaScript client has the same capability through `withAuthenticationRefresh` and `accessTokenFactory`. [Blazor Server](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-11.0) also benefits: interactive circuits receive the refreshed identity without reconnecting, and `AuthorizeView` re-renders when the roles or permissions change.

## Use One Authorization Model Everywhere

You can write a custom attribute that implements [`IAuthorizationRequirementData`](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-11.0) and apply it to a Minimal API endpoint to restrict access to it. However, if you apply the same attribute to an MVC controller, a SignalR hub, or a Blazor component nothing is enforced. Before .NET 11, these attributes were only honored on Minimal API and routed endpoints.

In .NET 11, MVC filters, SignalR hubs and hub methods, and Blazor's `AuthorizeView` and `AuthorizeRouteView` all evaluate authorization metadata the same way. Under the hood they share a new [`AuthorizationPolicy.CombineAsync`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicy.combineasync) overload:

```csharp
public static Task<AuthorizationPolicy?> CombineAsync(
    IAuthorizationPolicyProvider policyProvider,
    IEnumerable<object> metadata);
```

It merges `IAuthorizeData`, `AuthorizationPolicy` instances, and `IAuthorizationRequirementData` attributes using the same rules as the authorization middleware. Take the minimum age attribute from the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/custom-authorization-policies-with-iauthorizationrequirementdata?view=aspnetcore-11.0):

```csharp
class MinimumAgeAuthorizeAttribute(int age) : AuthorizeAttribute,
    IAuthorizationRequirement, IAuthorizationRequirementData
{
    public int Age { get; set; } = age;

    public IEnumerable<IAuthorizationRequirement> GetRequirements()
    {
        yield return this;
    }
}
```

You write the handler once and the attribute now works across the stack, for example on a hub:

```csharp
[MinimumAgeAuthorize(21)]
public class PokerTableHub : Hub { }
```

Notice that an attribute implementing both `IAuthorizeData` and `IAuthorizationRequirementData`, like the one above, contributes to the decision only once. Apps that opt out of endpoint routing (`EnableEndpointRouting = false`) keep the legacy MVC behavior.

The result is fewer surprises when you move an endpoint from one programming model to another. Your policy follows it.

## Give Passkeys Meaningful Names

[ASP.NET Core Identity gained passkey support in .NET 10](https://auth0.com/blog/authentication-authorization-enhancements-dotnet-10/), and it came with a small usability problem. Users who registered several passkeys saw a list of credentials with no hint of which device or password manager held which one.

.NET 11 improves this by inferring a display name from the authenticator's [AAGUID](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data#attestedcredentialdata), the identifier that tells you what kind of authenticator created the passkey. Built-in mappings cover common ones such as Google Password Manager, iCloud Keychain, Windows Hello, 1Password, and Bitwarden. When the authenticator is known, Identity assigns the name automatically. When it isn't, the user lands on a rename page.

If your organization issues its own hardware keys, you can add them to the `PasskeyAuthenticators` dictionary in your project. It's a small change, but it's the kind of detail that gives a passkey management page a professional look.

## Smaller Changes Worth Knowing

A few more items deserve a mention:

- **`TimeProvider` in ASP.NET Core Identity.** ASP.NET Core Identity now uses [`TimeProvider`](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider) instead of `DateTime` and `DateTimeOffset`. Register a `FakeTimeProvider` in your unit tests and you can check lockout durations, token expiration, and security stamp validation without waiting or mocking clocks.
- **[`dotnet user-jwts`](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-authn) for file-based apps.** Running `dotnet user-jwts create --file app.cs` creates a development JWT for a single-file app with no project file.
- **TLS channel binding for Negotiate.** On HTTPS, Kestrel now passes the TLS channel binding token to the Kerberos or NTLM exchange, which defends against relay attacks. There's nothing to configure. You can also read the token with `TryGetChannelBindingBytes` on [`ITlsConnectionFeature`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.features.itlsconnectionfeature).
- **`[ShortCircuit]`.** [This attribute](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11?view=aspnetcore-10.0&tabs=minimal-apis#short-circuit-endpoints-with-an-attribute) runs an endpoint right after routing and skips the rest of the pipeline, including authentication and CORS. Use it for health checks, never for anything that needs a user.

## What Didn't Make It: Device Bound Session Credentials

If you followed the earlier previews, you may remember an experimental package called `Microsoft.AspNetCore.Authentication.DeviceBoundSessions`. It implemented the [Device Bound Session Credentials](https://www.w3.org/TR/dbsc/) specification: a short-lived session cookie that the browser can only renew by proving it holds a private key, so a stolen cookie is useless after a few minutes.

The idea is promising, but the package was pulled before RC2. The ASP.NET Core team [found the implementation incomplete](https://github.com/dotnet/aspnetcore/pull/69462) and may bring it back in a later release. You can follow [the tracking issue](https://github.com/dotnet/aspnetcore/issues/66478) to keep up to date.

## Small Release, Sharper Edges

.NET 11 doesn’t bring any revolutionary changes to authentication, and that's fine. SignalR token refresh removes a trade-off that developers worked around for years. Consistent authorization metadata removes a class of silent gaps. Friendlier passkeys polish the experience that .NET 10 started.

Before you upgrade, check two things: allow the `/refresh` route in any proxy in front of your hubs, and review any custom authorization attributes you wrote for one endpoint type.

Which of these changes will you use first? Let me know in the comments.