| name | aspnetcore-authentication |
| description | ASP.NET Core authentication middleware configuration including OpenID Connect, JWT Bearer, cookie authentication, authentication schemes, challenge/forbid flows, and external identity provider integration. |
| invocable | false |
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
- Authentication ≠ Authorization — Authentication establishes who the user is. Authorization (see
aspnetcore-authorization) determines what they can do.
- 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.
- Cookies for Web Apps, JWT for APIs — Web applications use cookie authentication (with OIDC for login). APIs use JWT Bearer or introspection.
- 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.
- 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/tokens/authentication
Pattern 1: OIDC Authentication for Web Applications
The most common pattern — a server-rendered web app authenticating users via Duende IdentityServer:
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";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("api1");
options.Scope.Add("offline_access");
options.SaveTokens = true;
options.MapInboundClaims = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Critical Settings Explained
| Setting | Why | Default |
|---|
MapInboundClaims = false | Prevents renaming sub → http://schemas.xmlsoap.org/.../nameidentifier | true (maps) |
SaveTokens = true | Stores access/refresh tokens in the cookie for later API calls | false |
GetClaimsFromUserInfoEndpoint = true | Fetches full profile claims from userinfo | false |
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:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://identity.example.com";
options.Audience = "catalog-api";
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "catalog-api",
NameClaimType = "name",
RoleClaimType = "role"
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/products", () => Results.Ok())
.RequireAuthorization();
Multiple Audiences
When an API accepts tokens from multiple resources:
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:
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
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://identity.example.com";
options.MapInboundClaims = false;
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
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
options.DefaultForbidScheme = "oidc";
options.DefaultSignInScheme = "Cookies";
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 Mapping | After MapInboundClaims = false |
|---|
sub | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier | sub |
name | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name | name |
role | http://schemas.microsoft.com/ws/2008/06/identity/claims/role | role |
email | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress | email |
The Fix — Always Disable Mapping
options.MapInboundClaims = false;
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "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:
.AddOpenIdConnect("oidc", options =>
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.AcrValues = "tenant:myorg";
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var identity = context.Principal!.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim("app_version", "2.0"));
return Task.CompletedTask;
},
OnRedirectToIdentityProviderForSignOut = context =>
{
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
context.HandleResponse();
context.Response.Redirect("/error?message=" +
Uri.EscapeDataString(context.Failure?.Message ?? "Unknown error"));
return Task.CompletedTask;
}
};
});
Common Event Use Cases
| Event | Use Case |
|---|
OnRedirectToIdentityProvider | Add acr_values, login_hint, or custom parameters |
OnTokenValidated | Transform claims, load additional user data |
OnTokenResponseReceived | Inspect raw token response |
OnRemoteFailure | Custom error handling for failed logins |
OnSignedOutCallbackRedirect | Custom post-logout redirect |
Pattern 7: Sign-Out
Proper sign-out must clear both the local cookie and the IdentityServer session:
app.MapGet("/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync("Cookies");
await ctx.SignOutAsync("oidc");
});
The Sign-Out Flow
1. Client calls SignOutAsync("Cookies") → clears local cookie
2. Client calls SignOutAsync("oidc") → redirects to IS /connect/endsession
3. IdentityServer clears its session
4. IdentityServer notifies other clients → front-channel or back-channel logout
5. 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:
var 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");
httpClient.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.
Common Pitfalls
1. Forgetting MapInboundClaims
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://identity.example.com";
});
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://identity.example.com";
options.MapInboundClaims = false;
});
2. Missing UseAuthentication Before UseAuthorization
app.UseAuthorization();
app.UseAuthentication();
app.UseAuthentication();
app.UseAuthorization();
3. Not Clearing Scopes Before Adding
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api1");
options.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:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builder.Services.AddSingleton<ITicketStore, RedisTicketStore>();
.AddCookie("Cookies", options =>
{
options.SessionStore = app.Services.GetRequiredService<ITicketStore>();
});
.AddOpenIdConnect("oidc", options =>
{
options.ClaimActions.DeleteClaims("sid", "idp", "auth_time", "amr");
});
5. Redirect Loop After Login
Usually caused by the cookie not being set due to SameSite restrictions:
.AddCookie("Cookies", options =>
{
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});
Resources