Aspnetcore Authentication

by DuendeSoftwarefb32edc51982No license9 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 4 weeks ago

ASP.NET Core authentication middleware configuration including OpenID Connect, JWT Bearer, cookie authentication, authentication schemes, challenge/forbid flows, and external identity provider integration.

Instructions onlySoftware Development
AI-generated overview

Guides ASP.NET Core authentication middleware setup for OIDC, JWT Bearer, cookies, schemes and sign-out flows.

What it does
This skill provides configuration guidance for ASP.NET Core authentication middleware, covering OpenID Connect, JWT Bearer, cookie authentication, reference token introspection and mTLS certificate-bound tokens. It presents named authentication schemes, challenge, sign-in, sign-out and forbid flows, claim type mapping, handler events and common pitfalls. It produces code snippets and settings tables rather than files or scripts.
When to use it
Use it when configuring OIDC login for a web app, JWT Bearer validation for an API, or multiple authentication schemes. It also helps when debugging authentication failures such as 401 responses, redirect loops or claim mapping issues, and when integrating Duende IdentityServer as an identity provider.
Requirements
Requires an ASP.NET Core project and the relevant authentication packages, such as the OIDC and JWT Bearer handlers and Duende.AspNetCore.Authentication.JwtBearer for reference tokens. An OpenID Connect provider or IdentityServer instance is needed at runtime, plus client credentials or a client certificate for mTLS. Network access is needed for discovery and token endpoints. No scripts ship with the skill.

ASP.NET Core Authentication

When to Use This Skill

Use this skill when:

  • Configuring OIDC authentication in an ASP.NET Core web application
  • Setting up JWT Bearer authentication for an API
  • Managing authentication schemes (cookies, OIDC, JWT, external providers)
  • Implementing challenge, sign-in, sign-out, and forbid flows
  • Debugging authentication failures (401s, redirect loops, claim mapping issues)
  • Integrating with Duende IdentityServer as an OpenID Connect provider
  • Configuring token validation parameters

Core Principles

  1. Authentication ≠ Authorization — Authentication establishes who the user is. Authorization (see aspnetcore-authorization) determines what they can do.
  2. Scheme-Based Architecture — ASP.NET Core authentication is built around named schemes. Each scheme has a handler that knows how to authenticate, challenge, and sign out.
  3. Cookies for Web Apps, JWT for APIs — Web applications use cookie authentication (with OIDC for login). APIs use JWT Bearer or introspection.
  4. Never Roll Your Own — Use the built-in OIDC and JWT Bearer handlers. They handle nonce validation, key rotation, token validation, and dozens of edge cases.
  5. Claim Type Mapping Matters — The OIDC handler maps JWT claim types to .NET claim types by default. Disable this for predictable claim names.

Related Skills

  • aspnetcore-authorization — Policy-based authorization after authentication
  • identityserver-configuration — Server-side client and resource configuration
  • identityserver-sessions-providers — Server-side sessions to reduce cookie size and maintain IdP-side data
  • oauth-oidc-protocols — Protocol fundamentals underlying these handlers
  • token-management — Automatic token refresh with Duende.AccessTokenManagement

Docs: https://docs.duendesoftware.com/identityserver/apis/aspnetcore/jwt/


Pattern 1: OIDC Authentication for Web Applications

The most common pattern — a server-rendered web app authenticating users via Duende IdentityServer:

csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options =>{    options.DefaultScheme = "Cookies";    options.DefaultChallengeScheme = "oidc";}).AddCookie("Cookies", options =>{    options.Cookie.Name = "myapp";    options.Cookie.SameSite = SameSiteMode.Lax;    options.ExpireTimeSpan = TimeSpan.FromHours(8);    options.SlidingExpiration = true;}).AddOpenIdConnect("oidc", options =>{    options.Authority = "https://identity.example.com";    options.ClientId = "web.app";    options.ClientSecret = "secret";    options.ResponseType = "code"; // Authorization code flow
    // Map scopes to request    options.Scope.Clear();    options.Scope.Add("openid");    options.Scope.Add("profile");    options.Scope.Add("email");    options.Scope.Add("api1");    options.Scope.Add("offline_access"); // For refresh tokens
    // Save tokens in the authentication cookie    options.SaveTokens = true;
    // Disable Microsoft's JWT claim type mapping    options.MapInboundClaims = false;
    // Where to get additional user claims    options.GetClaimsFromUserInfoEndpoint = true;
    options.TokenValidationParameters = new TokenValidationParameters    {        NameClaimType = "name",        RoleClaimType = "role"    };});
var app = builder.Build();app.UseAuthentication();app.UseAuthorization();

Critical Settings Explained

SettingWhyDefault
MapInboundClaims = falsePrevents renaming sub → http://schemas.xmlsoap.org/.../nameidentifiertrue (maps)
SaveTokens = trueStores access/refresh tokens in the cookie for later API callsfalse
GetClaimsFromUserInfoEndpoint = trueFetches full profile claims from userinfofalse
ResponseType = "code"Authorization code flow (PKCE is automatic in .NET 7+)"code" (.NET 7+; was "code id_token" in earlier versions)

Pattern 2: JWT Bearer Authentication for APIs

APIs validate access tokens issued by IdentityServer:

csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication("Bearer")    .AddJwtBearer("Bearer", options =>    {        options.Authority = "https://identity.example.com";        options.Audience = "catalog-api"; // Must match ApiResource name
        options.MapInboundClaims = false; // Must be included
        options.TokenValidationParameters = new TokenValidationParameters        {            ValidateAudience = true,            ValidAudience = "catalog-api",            NameClaimType = "name",            RoleClaimType = "role"        };    });
builder.Services.AddAuthorization();
var app = builder.Build();app.UseAuthentication();app.UseAuthorization();
// Protect endpointsapp.MapGet("/products", () => Results.Ok())    .RequireAuthorization();

Multiple Audiences

When an API accepts tokens from multiple resources:

csharp
options.TokenValidationParameters = new TokenValidationParameters{    ValidateAudience = true,    ValidAudiences = new[] { "catalog-api", "shared-api" }};

Pattern 3: Reference Token Introspection

For APIs that validate reference tokens (opaque tokens) instead of JWTs:

csharp
builder.Services.AddAuthentication("Bearer")    .AddOAuth2Introspection("Bearer", options =>    {        options.Authority = "https://identity.example.com";        options.ClientId = "catalog-api";        options.ClientSecret = "api-secret";    });

Install the Duende.AspNetCore.Authentication.JwtBearer package which supports both JWT and reference token validation, switching automatically based on the token format.

Combined JWT + Reference Token Support

csharp
builder.Services.AddAuthentication("Bearer")    .AddJwtBearer("Bearer", options =>    {        options.Authority = "https://identity.example.com";        options.MapInboundClaims = false;
        // The Duende JWT handler can forward to introspection for reference tokens        options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");    })    .AddOAuth2Introspection("introspection", options =>    {        options.Authority = "https://identity.example.com";        options.ClientId = "catalog-api";        options.ClientSecret = "api-secret";    });

Pattern 4: Understanding Authentication Schemes

ASP.NET Core uses named authentication schemes. Each scheme is handled by a specific handler.

Default Schemes

csharp
builder.Services.AddAuthentication(options =>{    // Used for [Authorize] attribute and User.Identity    options.DefaultScheme = "Cookies";
    // Used when authentication is required (401 → redirect to login)    options.DefaultChallengeScheme = "oidc";
    // Used when access is denied (403)    options.DefaultForbidScheme = "oidc";
    // Used when signing in (setting the cookie after OIDC callback)    options.DefaultSignInScheme = "Cookies";
    // Used when signing out    options.DefaultSignOutScheme = "oidc";});

The Authentication Flow

Request → [UseAuthentication] → Cookie handler reads cookie                                  ├─ Valid cookie → User is authenticated                                  └─ No cookie → User is anonymous         [UseAuthorization]  → [Authorize] attribute checks                                  ├─ Authenticated → proceed                                  └─ Not authenticated → Challenge                                       └─ OIDC handler redirects to IdentityServer                                            └─ User logs in → callback → cookie created

Pattern 5: Claim Type Mapping

By default, the Microsoft OIDC handler remaps JWT claims to XML-based .NET claim types. This causes confusion:

The Mapping Problem

JWT Claim.NET Default MappingAfter MapInboundClaims = false
subhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifiersub
namehttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/namename
rolehttp://schemas.microsoft.com/ws/2008/06/identity/claims/rolerole
emailhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddressemail

The Fix — Always Disable Mapping

csharp
// ✅ On the OIDC handleroptions.MapInboundClaims = false;
// ✅ On the JWT Bearer handleroptions.MapInboundClaims = false;
// ✅ Then tell ASP.NET Core which claims to use for Name and Roleoptions.TokenValidationParameters = new TokenValidationParameters{    NameClaimType = "name", // Strongly recommended to use "name"    RoleClaimType = "role"  // Strongly recommended to use "role"};

Why this matters: Without this, User.FindFirst("sub") returns null because the claim was renamed. You'd need to use the verbose XML URI instead.


Pattern 6: OIDC Handler Events

The OIDC handler exposes events for customizing the authentication pipeline:

csharp
.AddOpenIdConnect("oidc", options =>{    // ... other options ...
    options.Events = new OpenIdConnectEvents    {        // Customize the authorize request (e.g., add acr_values)        OnRedirectToIdentityProvider = context =>        {            context.ProtocolMessage.AcrValues = "tenant:myorg";            return Task.CompletedTask;        },
        // Handle tokens after successful authentication        OnTokenValidated = context =>        {            // Add custom claims to the identity            var identity = context.Principal!.Identity as ClaimsIdentity;            identity?.AddClaim(new Claim("app_version", "2.0"));            return Task.CompletedTask;        },
        // Handle sign-out redirect        OnRedirectToIdentityProviderForSignOut = context =>        {            // Customize the logout redirect            return Task.CompletedTask;        },
        // Handle failures        OnRemoteFailure = context =>        {            context.HandleResponse();            context.Response.Redirect("/error?message=" +                Uri.EscapeDataString(context.Failure?.Message ?? "Unknown error"));            return Task.CompletedTask;        }    };});

Common Event Use Cases

EventUse Case
OnRedirectToIdentityProviderAdd acr_values, login_hint, or custom parameters
OnTokenValidatedTransform claims, load additional user data
OnTokenResponseReceivedInspect raw token response
OnRemoteFailureCustom error handling for failed logins
OnSignedOutCallbackRedirectCustom post-logout redirect

Pattern 7: Sign-Out

Proper sign-out must clear both the local cookie and the IdentityServer session:

csharp
// In a Razor Page or Controllerapp.MapGet("/logout", async (HttpContext ctx) =>{    // Signs out of both the cookie and IdentityServer    await ctx.SignOutAsync("Cookies");    await ctx.SignOutAsync("oidc");});

The Sign-Out Flow

1. Client calls SignOutAsync("Cookies")    → clears local cookie2. Client calls SignOutAsync("oidc")       → redirects to IS /connect/endsession3. IdentityServer clears its session4. IdentityServer notifies other clients   → front-channel or back-channel logout5. IdentityServer redirects to PostLogoutRedirectUri

Important: Calling only SignOutAsync("Cookies") without SignOutAsync("oidc") leaves the IdentityServer session active. The user will be silently re-authenticated on the next challenge.


Pattern 8: Accessing Stored Tokens

When SaveTokens = true, the access token, refresh token, and ID token are stored in the authentication cookie:

csharp
// In a controller or middlewarevar accessToken = await HttpContext.GetTokenAsync("access_token");var refreshToken = await HttpContext.GetTokenAsync("refresh_token");var idToken = await HttpContext.GetTokenAsync("id_token");var expiresAt = await HttpContext.GetTokenAsync("expires_at");
// Use the access token to call an APIhttpClient.SetBearerToken(accessToken);

Better approach: Use Duende.AccessTokenManagement (see token-management skill) which handles token refresh, caching, and rotation automatically instead of manually managing stored tokens.


Pattern 9: mTLS (Certificate-Bound Tokens) with the OIDC Handler

When IdentityServer issues certificate-bound tokens via mTLS (RFC 8705), the OIDC client must (1) present its client certificate on all back-channel calls and (2) target the mTLS endpoint aliases. The stock handler does neither on its own.

Step 1 — Present the client certificate on back-channel calls

Set BackchannelHttpHandler so code redemption, refresh, and userinfo calls run over a mutually-authenticated TLS channel. No client secret is needed — the certificate authenticates the client:

csharp
var clientCert = X509CertificateLoader.LoadPkcs12(File.ReadAllBytes("client.p12"), "password");
.AddOpenIdConnect("oidc", options =>{    options.Authority = "https://identity.example.com";    options.ClientId = "mtls.client";    // no ClientSecret — the certificate authenticates the client    options.ResponseType = "code";    options.MapInboundClaims = false;    options.SaveTokens = true;
    options.BackchannelHttpHandler = new SocketsHttpHandler    {        SslOptions = new SslClientAuthenticationOptions        {            ClientCertificates = new X509CertificateCollection { clientCert }        }    };});

Step 2 — Point the handler at mtls_endpoint_aliases

The stock OIDC handler reads endpoints from standard discovery metadata and does not understand mtls_endpoint_aliases — it would call the non-mTLS token_endpoint. Wrap the standard configuration manager and rewrite the endpoints to their mTLS aliases:

csharp
public sealed class MtlsConfigurationManager : IConfigurationManager<OpenIdConnectConfiguration>{    private readonly ConfigurationManager<OpenIdConnectConfiguration> _inner;
    public MtlsConfigurationManager(ConfigurationManager<OpenIdConnectConfiguration> inner)        => _inner = inner;
    public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(CancellationToken ct)    {        var config = await _inner.GetConfigurationAsync(ct);
        if (config.AdditionalData.TryGetValue("mtls_endpoint_aliases", out var raw)            && raw is JsonElement aliases)        {            config.TokenEndpoint                      = aliases.GetProperty("token_endpoint").GetString();            config.IntrospectionEndpoint              = aliases.GetProperty("introspection_endpoint").GetString();            config.DeviceAuthorizationEndpoint        = aliases.GetProperty("device_authorization_endpoint").GetString();            // .NET 9+ auto-uses PAR when advertised — rewrite it too, or the handler            // pushes to the non-mTLS PAR endpoint and the certificate binding is lost            config.PushedAuthorizationRequestEndpoint = aliases.GetProperty("pushed_authorization_request_endpoint").GetString();            // revocation has no strongly-typed slot — keep the mTLS value in AdditionalData            config.AdditionalData["revocation_endpoint"] = aliases.GetProperty("revocation_endpoint").GetString();        }
        return config;    }
    public void RequestRefresh() => _inner.RequestRefresh();}
// Wire it onto the handler.AddOpenIdConnect("oidc", options =>{    // ... options from Step 1 ...    options.ConfigurationManager = new MtlsConfigurationManager(        new ConfigurationManager<OpenIdConnectConfiguration>(            $"{options.Authority}/.well-known/openid-configuration",            new OpenIdConnectConfigurationRetriever(),            new HttpDocumentRetriever { RequireHttps = true }));});

PAR + mTLS on .NET 9+: because the handler automatically uses PAR when the server advertises a pushed_authorization_request_endpoint, you must rewrite that endpoint to the mTLS alias as well. Otherwise the pushed authorization request goes to the non-mTLS endpoint and the certificate binding is lost.


Common Pitfalls

1. Forgetting MapInboundClaims

csharp
// ❌ WRONG — Claims have XML URIs, User.FindFirst("sub") returns null.AddOpenIdConnect("oidc", options =>{    options.Authority = "https://identity.example.com";    // MapInboundClaims defaults to true});
// ✅ CORRECT.AddOpenIdConnect("oidc", options =>{    options.Authority = "https://identity.example.com";    options.MapInboundClaims = false;});

2. Missing UseAuthentication Before UseAuthorization

csharp
// ❌ WRONG — Authorization middleware can't see the authenticated userapp.UseAuthorization();app.UseAuthentication(); // Too late!
// ✅ CORRECT — Authentication must come firstapp.UseAuthentication();app.UseAuthorization();

3. Not Clearing Scopes Before Adding

csharp
// ❌ WRONG — Default scopes (openid, profile) are already addedoptions.Scope.Add("openid");   // Duplicate!options.Scope.Add("profile");  // Duplicate!options.Scope.Add("api1");
// ✅ CORRECT — Clear defaults firstoptions.Scope.Clear();options.Scope.Add("openid");options.Scope.Add("profile");options.Scope.Add("api1");

4. Cookie Too Large (>4KB)

When SaveTokens = true and many claims are included, the cookie can exceed browser limits:

csharp
// ✅ Solution 1: Use a server-side ITicketStore to move auth ticket out of the cookie.// Implement ITicketStore backed by IDistributedCache (e.g., Redis), then register it:builder.Services.AddStackExchangeRedisCache(options =>{    options.Configuration = "localhost:6379";});builder.Services.AddSingleton<ITicketStore, RedisTicketStore>(); // your ITicketStore impl
.AddCookie("Cookies", options =>{    // Wire the ITicketStore so the cookie only holds a session key, not the full ticket    options.SessionStore = app.Services.GetRequiredService<ITicketStore>();});// Note: ITicketStore is in Microsoft.AspNetCore.Authentication.Cookies namespace.// There is no built-in DistributedSessionStore class — you must implement ITicketStore.
// ✅ Solution 2: Filter claims stored in the cookie.AddOpenIdConnect("oidc", options =>{    options.ClaimActions.DeleteClaims("sid", "idp", "auth_time", "amr");});
// ✅ Solution 3: Use Duende IdentityServer server-side sessions

5. Redirect Loop After Login

Usually caused by the cookie not being set due to SameSite restrictions:

csharp
// ✅ Check SameSite settings.AddCookie("Cookies", options =>{    options.Cookie.SameSite = SameSiteMode.Lax; // Not Strict for OIDC callbacks    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;});

Resources

Source and attribution

Source:DuendeSoftware/duende-skillsinskills/aspnetcore-authenticationat commitfb32edc

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

Aspnetcore Authentication Agent Skill | SourceWeft