Aspnetcore Authentication

作者 DuendeSoftwarefb32edc51982無授權條款9 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫4 週前更新

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

AI 產生的概覽

指導 ASP.NET Core 驗證中介軟體設定,涵蓋 OIDC、JWT Bearer、Cookie、配置與登出流程。

功能
此技能提供 ASP.NET Core 驗證中介軟體的設定指引,涵蓋 OpenID Connect、JWT Bearer、Cookie 驗證、參考權杖內省以及 mTLS 憑證綁定權杖。內容說明具名驗證配置、挑戰、登入、登出與禁止流程、宣告類型對應、處理常式事件以及常見陷阱。產出為程式碼片段與設定表格,而非檔案或指令碼。
適用情境
適用於為網頁應用程式設定 OIDC 登入、為 API 設定 JWT Bearer 驗證,或管理多種驗證配置。也適合排查驗證失敗,例如 401 回應、重新導向迴圈或宣告對應問題,以及將 Duende IdentityServer 整合為身分識別提供者時使用。
執行需求
需要一個 ASP.NET Core 專案及相關驗證套件,例如 OIDC 與 JWT Bearer 處理常式,以及用於參考權杖的 Duende.AspNetCore.Authentication.JwtBearer。執行時需要 OpenID Connect 提供者或 IdentityServer 執行個體,並需要用戶端認證或供 mTLS 使用的用戶端憑證。探索與權杖端點需要網路存取。此技能不隨附指令碼。

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

來源與署名

來源:DuendeSoftware/duende-skills位於skills/aspnetcore-authentication提交fb32edc

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架