[{"data":1,"prerenderedAt":1186},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-authentication":3},{"id":4,"title":5,"body":6,"description":1172,"difficulty":1173,"extension":1174,"framework":1175,"frameworkSlug":1176,"meta":1177,"navigation":83,"order":56,"path":1178,"qaPath":1179,"seo":1180,"stem":1181,"subtopic":30,"topic":1182,"topicSlug":1183,"updated":1184,"__hash__":1185},"blog\u002Fblog\u002Fdotnet-authentication.md","Authentication in ASP.NET Core",{"type":7,"value":8,"toc":1157},"minimark",[9,14,18,22,25,37,43,103,118,122,132,294,297,301,315,378,381,385,390,475,479,485,512,529,533,536,612,625,629,632,681,720,723,752,756,763,928,938,942,945,1016,1019,1023,1026,1069,1072,1076,1125,1129,1153],[10,11,13],"h2",{"id":12},"why-authentication-knowledge-matters-in-net-interviews","Why authentication knowledge matters in .NET interviews",[15,16,17],"p",{},"Authentication is one of the first things that goes wrong in production systems, and interviewers\nknow it. They probe not just \"how do you add auth?\" but whether you understand the pipeline order,\nthe difference between challenge and forbid, how claims are populated, and when to reach for a\ncustom handler. This article walks through the fundamentals end to end.",[10,19,21],{"id":20},"authentication-vs-authorization-the-critical-distinction","Authentication vs authorization — the critical distinction",[15,23,24],{},"These two words are often confused, but ASP.NET Core treats them as completely separate concerns\nwith separate middleware.",[15,26,27,31,32,36],{},[28,29,30],"strong",{},"Authentication"," answers \"who are you?\" — it reads a credential (cookie, token, API key) and\nestablishes the caller's identity as a ",[33,34,35],"code",{},"ClaimsPrincipal",".",[15,38,39,42],{},[28,40,41],{},"Authorization"," answers \"what are you allowed to do?\" — it evaluates the established identity\nagainst roles, policies, and requirements.",[44,45,50],"pre",{"className":46,"code":47,"language":48,"meta":49,"style":49},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Correct pipeline order — authentication must populate User before authorization reads it:\napp.UseRouting();\napp.UseAuthentication(); \u002F\u002F populate HttpContext.User\napp.UseAuthorization();  \u002F\u002F check permissions against that User\n\n\u002F\u002F Swapped order — every request appears anonymous to the authorization check:\n\u002F\u002F app.UseAuthorization();\n\u002F\u002F app.UseAuthentication();\n","csharp","",[33,51,52,60,66,72,78,85,91,97],{"__ignoreMap":49},[53,54,57],"span",{"class":55,"line":56},"line",1,[53,58,59],{},"\u002F\u002F Correct pipeline order — authentication must populate User before authorization reads it:\n",[53,61,63],{"class":55,"line":62},2,[53,64,65],{},"app.UseRouting();\n",[53,67,69],{"class":55,"line":68},3,[53,70,71],{},"app.UseAuthentication(); \u002F\u002F populate HttpContext.User\n",[53,73,75],{"class":55,"line":74},4,[53,76,77],{},"app.UseAuthorization();  \u002F\u002F check permissions against that User\n",[53,79,81],{"class":55,"line":80},5,[53,82,84],{"emptyLinePlaceholder":83},true,"\n",[53,86,88],{"class":55,"line":87},6,[53,89,90],{},"\u002F\u002F Swapped order — every request appears anonymous to the authorization check:\n",[53,92,94],{"class":55,"line":93},7,[53,95,96],{},"\u002F\u002F app.UseAuthorization();\n",[53,98,100],{"class":55,"line":99},8,[53,101,102],{},"\u002F\u002F app.UseAuthentication();\n",[15,104,105,106,109,110,113,114,117],{},"Anonymous user hitting ",[33,107,108],{},"[Authorize]"," produces ",[28,111,112],{},"401 Challenge"," (not logged in).\nAuthenticated user lacking permission produces ",[28,115,116],{},"403 Forbid"," (logged in but not allowed).",[10,119,121],{"id":120},"how-cookie-authentication-works","How cookie authentication works",[15,123,124,125,127,128,131],{},"Cookie authentication is the simplest and most common scheme for server-rendered apps and\nsame-origin SPAs. The sign-in handler serializes the ",[33,126,35],{}," into an encrypted,\ntamper-proof cookie. On every subsequent request, the middleware decrypts it and restores\n",[33,129,130],{},"HttpContext.User"," — no database query per request.",[44,133,135],{"className":46,"code":134,"language":48,"meta":49,"style":49},"\u002F\u002F Register:\nbuilder.Services\n    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)\n    .AddCookie(options =>\n    {\n        options.LoginPath         = \"\u002Faccount\u002Flogin\";   \u002F\u002F 401 → redirect here\n        options.AccessDeniedPath  = \"\u002Faccount\u002Fdenied\";  \u002F\u002F 403 → redirect here\n        options.ExpireTimeSpan    = TimeSpan.FromHours(8);\n        options.SlidingExpiration = true;               \u002F\u002F reset on each request\n    });\n\n\u002F\u002F Sign in — after validating credentials:\nvar claims = new List\u003CClaim>\n{\n    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),\n    new Claim(ClaimTypes.Email,           user.Email),\n    new Claim(ClaimTypes.Role,            user.Role),\n};\nvar identity  = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);\nvar principal = new ClaimsPrincipal(identity);\n\nawait HttpContext.SignInAsync(\n    CookieAuthenticationDefaults.AuthenticationScheme,\n    principal,\n    new AuthenticationProperties { IsPersistent = dto.RememberMe });\n\n\u002F\u002F Sign out:\nawait HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n",[33,136,137,142,147,152,157,162,167,172,177,183,189,194,200,206,212,218,224,230,236,242,248,253,259,265,271,277,282,288],{"__ignoreMap":49},[53,138,139],{"class":55,"line":56},[53,140,141],{},"\u002F\u002F Register:\n",[53,143,144],{"class":55,"line":62},[53,145,146],{},"builder.Services\n",[53,148,149],{"class":55,"line":68},[53,150,151],{},"    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)\n",[53,153,154],{"class":55,"line":74},[53,155,156],{},"    .AddCookie(options =>\n",[53,158,159],{"class":55,"line":80},[53,160,161],{},"    {\n",[53,163,164],{"class":55,"line":87},[53,165,166],{},"        options.LoginPath         = \"\u002Faccount\u002Flogin\";   \u002F\u002F 401 → redirect here\n",[53,168,169],{"class":55,"line":93},[53,170,171],{},"        options.AccessDeniedPath  = \"\u002Faccount\u002Fdenied\";  \u002F\u002F 403 → redirect here\n",[53,173,174],{"class":55,"line":99},[53,175,176],{},"        options.ExpireTimeSpan    = TimeSpan.FromHours(8);\n",[53,178,180],{"class":55,"line":179},9,[53,181,182],{},"        options.SlidingExpiration = true;               \u002F\u002F reset on each request\n",[53,184,186],{"class":55,"line":185},10,[53,187,188],{},"    });\n",[53,190,192],{"class":55,"line":191},11,[53,193,84],{"emptyLinePlaceholder":83},[53,195,197],{"class":55,"line":196},12,[53,198,199],{},"\u002F\u002F Sign in — after validating credentials:\n",[53,201,203],{"class":55,"line":202},13,[53,204,205],{},"var claims = new List\u003CClaim>\n",[53,207,209],{"class":55,"line":208},14,[53,210,211],{},"{\n",[53,213,215],{"class":55,"line":214},15,[53,216,217],{},"    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),\n",[53,219,221],{"class":55,"line":220},16,[53,222,223],{},"    new Claim(ClaimTypes.Email,           user.Email),\n",[53,225,227],{"class":55,"line":226},17,[53,228,229],{},"    new Claim(ClaimTypes.Role,            user.Role),\n",[53,231,233],{"class":55,"line":232},18,[53,234,235],{},"};\n",[53,237,239],{"class":55,"line":238},19,[53,240,241],{},"var identity  = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);\n",[53,243,245],{"class":55,"line":244},20,[53,246,247],{},"var principal = new ClaimsPrincipal(identity);\n",[53,249,251],{"class":55,"line":250},21,[53,252,84],{"emptyLinePlaceholder":83},[53,254,256],{"class":55,"line":255},22,[53,257,258],{},"await HttpContext.SignInAsync(\n",[53,260,262],{"class":55,"line":261},23,[53,263,264],{},"    CookieAuthenticationDefaults.AuthenticationScheme,\n",[53,266,268],{"class":55,"line":267},24,[53,269,270],{},"    principal,\n",[53,272,274],{"class":55,"line":273},25,[53,275,276],{},"    new AuthenticationProperties { IsPersistent = dto.RememberMe });\n",[53,278,280],{"class":55,"line":279},26,[53,281,84],{"emptyLinePlaceholder":83},[53,283,285],{"class":55,"line":284},27,[53,286,287],{},"\u002F\u002F Sign out:\n",[53,289,291],{"class":55,"line":290},28,[53,292,293],{},"await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n",[15,295,296],{},"The cookie is encrypted with ASP.NET Core Data Protection. In production, configure a shared,\npersistent key store (file system, Redis, Azure Blob) — otherwise each restart or scale-out\nnode generates new keys and invalidates all existing sessions.",[10,298,300],{"id":299},"claims-based-identity-the-data-model","Claims-based identity — the data model",[15,302,303,304,306,307,310,311,314],{},"Every authenticated user is represented as a ",[33,305,35],{}," containing one or more\n",[33,308,309],{},"ClaimsIdentity"," objects, each holding a flat list of ",[33,312,313],{},"Claim"," (type, value, issuer) triples.",[44,316,318],{"className":46,"code":317,"language":48,"meta":49,"style":49},"\u002F\u002F Reading claims — always use FindFirstValue for nullable access:\nvar userId = User.FindFirstValue(ClaimTypes.NameIdentifier); \u002F\u002F null if not authenticated\nvar email  = User.FindFirstValue(ClaimTypes.Email);\nvar roles  = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();\n\n\u002F\u002F Boolean helpers:\nbool isAuth  = User.Identity?.IsAuthenticated ?? false;\nbool isAdmin = User.IsInRole(\"Admin\"); \u002F\u002F checks ClaimTypes.Role claims\nstring? name = User.Identity?.Name;   \u002F\u002F value of ClaimTypes.Name claim\n\n\u002F\u002F Custom claims — any string key is valid:\nvar tenantId = User.FindFirstValue(\"tenant_id\");\n",[33,319,320,325,330,335,340,344,349,354,359,364,368,373],{"__ignoreMap":49},[53,321,322],{"class":55,"line":56},[53,323,324],{},"\u002F\u002F Reading claims — always use FindFirstValue for nullable access:\n",[53,326,327],{"class":55,"line":62},[53,328,329],{},"var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); \u002F\u002F null if not authenticated\n",[53,331,332],{"class":55,"line":68},[53,333,334],{},"var email  = User.FindFirstValue(ClaimTypes.Email);\n",[53,336,337],{"class":55,"line":74},[53,338,339],{},"var roles  = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();\n",[53,341,342],{"class":55,"line":80},[53,343,84],{"emptyLinePlaceholder":83},[53,345,346],{"class":55,"line":87},[53,347,348],{},"\u002F\u002F Boolean helpers:\n",[53,350,351],{"class":55,"line":93},[53,352,353],{},"bool isAuth  = User.Identity?.IsAuthenticated ?? false;\n",[53,355,356],{"class":55,"line":99},[53,357,358],{},"bool isAdmin = User.IsInRole(\"Admin\"); \u002F\u002F checks ClaimTypes.Role claims\n",[53,360,361],{"class":55,"line":179},[53,362,363],{},"string? name = User.Identity?.Name;   \u002F\u002F value of ClaimTypes.Name claim\n",[53,365,366],{"class":55,"line":185},[53,367,84],{"emptyLinePlaceholder":83},[53,369,370],{"class":55,"line":191},[53,371,372],{},"\u002F\u002F Custom claims — any string key is valid:\n",[53,374,375],{"class":55,"line":196},[53,376,377],{},"var tenantId = User.FindFirstValue(\"tenant_id\");\n",[15,379,380],{},"Never store passwords, SSNs, or payment information in claims. Claims live in the cookie or\ntoken where any party can decode (though not forge) them.",[10,382,384],{"id":383},"authentication-schemes-using-multiple-at-once","Authentication schemes — using multiple at once",[15,386,387,388,36],{},"A scheme is a named registration of an authentication handler. Multiple schemes coexist; the\ndefault scheme is used when no scheme is specified on ",[33,389,108],{},[44,391,393],{"className":46,"code":392,"language":48,"meta":49,"style":49},"builder.Services\n    .AddAuthentication(options =>\n    {\n        options.DefaultScheme          = CookieAuthenticationDefaults.AuthenticationScheme;\n        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;\n    })\n    .AddCookie()\n    .AddJwtBearer(options => { options.Authority = \"https:\u002F\u002Fauth.example.com\"; options.Audience = \"api1\"; })\n    .AddGoogle(options =>\n    {\n        options.ClientId     = config[\"Auth:Google:ClientId\"]!;\n        options.ClientSecret = config[\"Auth:Google:ClientSecret\"]!;\n    });\n\n\u002F\u002F Target a specific scheme per endpoint:\n[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]\npublic class ApiController : ControllerBase { }\n",[33,394,395,399,404,408,413,418,423,428,433,438,442,447,452,456,460,465,470],{"__ignoreMap":49},[53,396,397],{"class":55,"line":56},[53,398,146],{},[53,400,401],{"class":55,"line":62},[53,402,403],{},"    .AddAuthentication(options =>\n",[53,405,406],{"class":55,"line":68},[53,407,161],{},[53,409,410],{"class":55,"line":74},[53,411,412],{},"        options.DefaultScheme          = CookieAuthenticationDefaults.AuthenticationScheme;\n",[53,414,415],{"class":55,"line":80},[53,416,417],{},"        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;\n",[53,419,420],{"class":55,"line":87},[53,421,422],{},"    })\n",[53,424,425],{"class":55,"line":93},[53,426,427],{},"    .AddCookie()\n",[53,429,430],{"class":55,"line":99},[53,431,432],{},"    .AddJwtBearer(options => { options.Authority = \"https:\u002F\u002Fauth.example.com\"; options.Audience = \"api1\"; })\n",[53,434,435],{"class":55,"line":179},[53,436,437],{},"    .AddGoogle(options =>\n",[53,439,440],{"class":55,"line":185},[53,441,161],{},[53,443,444],{"class":55,"line":191},[53,445,446],{},"        options.ClientId     = config[\"Auth:Google:ClientId\"]!;\n",[53,448,449],{"class":55,"line":196},[53,450,451],{},"        options.ClientSecret = config[\"Auth:Google:ClientSecret\"]!;\n",[53,453,454],{"class":55,"line":202},[53,455,188],{},[53,457,458],{"class":55,"line":208},[53,459,84],{"emptyLinePlaceholder":83},[53,461,462],{"class":55,"line":214},[53,463,464],{},"\u002F\u002F Target a specific scheme per endpoint:\n",[53,466,467],{"class":55,"line":220},[53,468,469],{},"[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]\n",[53,471,472],{"class":55,"line":226},[53,473,474],{},"public class ApiController : ControllerBase { }\n",[10,476,478],{"id":477},"how-httpcontextuser-is-populated","How HttpContext.User is populated",[15,480,481,484],{},[33,482,483],{},"UseAuthentication"," middleware fires early in every request:",[486,487,488,495,498,504],"ol",{},[489,490,491,492,36],"li",{},"Calls ",[33,493,494],{},"IAuthenticationService.AuthenticateAsync(defaultScheme)",[489,496,497],{},"The handler reads the credential (decrypts cookie, validates JWT).",[489,499,500,501,36],{},"On success, sets ",[33,502,503],{},"HttpContext.User = ticket.Principal",[489,505,506,507,509,510,36],{},"On failure\u002Fabsence, sets ",[33,508,130],{}," to an anonymous ",[33,511,35],{},[15,513,514,516,517,520,521,524,525,528],{},[33,515,130],{}," is never null — check ",[33,518,519],{},"IsAuthenticated"," before reading claims. Access\nthe user outside HTTP context via ",[33,522,523],{},"IHttpContextAccessor"," (register with\n",[33,526,527],{},"builder.Services.AddHttpContextAccessor()","), but avoid this in domain services — pass\nthe user ID as a parameter instead.",[10,530,532],{"id":531},"signinasync-signoutasync-and-authenticateasync","SignInAsync, SignOutAsync, and AuthenticateAsync",[15,534,535],{},"These are the three operations the authentication system exposes:",[44,537,539],{"className":46,"code":538,"language":48,"meta":49,"style":49},"\u002F\u002F SignInAsync — persist the principal (write cookie, issue token, etc.):\nawait HttpContext.SignInAsync(scheme, principal, properties);\n\n\u002F\u002F SignOutAsync — remove the persistence (clear cookie, revoke session):\nawait HttpContext.SignOutAsync(scheme);\n\n\u002F\u002F AuthenticateAsync — read\u002Fvalidate the current credential (called by middleware):\nvar result = await HttpContext.AuthenticateAsync();\nif (result.Succeeded) { var user = result.Principal; }\n\n\u002F\u002F ChallengeAsync — trigger 401 (cookie → redirect to login; JWT → WWW-Authenticate header):\nawait HttpContext.ChallengeAsync();\n\n\u002F\u002F ForbidAsync — trigger 403 (cookie → redirect to access denied; JWT → 403 body):\nawait HttpContext.ForbidAsync();\n",[33,540,541,546,551,555,560,565,569,574,579,584,588,593,598,602,607],{"__ignoreMap":49},[53,542,543],{"class":55,"line":56},[53,544,545],{},"\u002F\u002F SignInAsync — persist the principal (write cookie, issue token, etc.):\n",[53,547,548],{"class":55,"line":62},[53,549,550],{},"await HttpContext.SignInAsync(scheme, principal, properties);\n",[53,552,553],{"class":55,"line":68},[53,554,84],{"emptyLinePlaceholder":83},[53,556,557],{"class":55,"line":74},[53,558,559],{},"\u002F\u002F SignOutAsync — remove the persistence (clear cookie, revoke session):\n",[53,561,562],{"class":55,"line":80},[53,563,564],{},"await HttpContext.SignOutAsync(scheme);\n",[53,566,567],{"class":55,"line":87},[53,568,84],{"emptyLinePlaceholder":83},[53,570,571],{"class":55,"line":93},[53,572,573],{},"\u002F\u002F AuthenticateAsync — read\u002Fvalidate the current credential (called by middleware):\n",[53,575,576],{"class":55,"line":99},[53,577,578],{},"var result = await HttpContext.AuthenticateAsync();\n",[53,580,581],{"class":55,"line":179},[53,582,583],{},"if (result.Succeeded) { var user = result.Principal; }\n",[53,585,586],{"class":55,"line":185},[53,587,84],{"emptyLinePlaceholder":83},[53,589,590],{"class":55,"line":191},[53,591,592],{},"\u002F\u002F ChallengeAsync — trigger 401 (cookie → redirect to login; JWT → WWW-Authenticate header):\n",[53,594,595],{"class":55,"line":196},[53,596,597],{},"await HttpContext.ChallengeAsync();\n",[53,599,600],{"class":55,"line":202},[53,601,84],{"emptyLinePlaceholder":83},[53,603,604],{"class":55,"line":208},[53,605,606],{},"\u002F\u002F ForbidAsync — trigger 403 (cookie → redirect to access denied; JWT → 403 body):\n",[53,608,609],{"class":55,"line":214},[53,610,611],{},"await HttpContext.ForbidAsync();\n",[15,613,614,615,618,619,622,623,36],{},"For JWT-based APIs, ",[33,616,617],{},"SignInAsync"," is not used — you generate and return the token yourself\nin the login response. The middleware only calls ",[33,620,621],{},"AuthenticateAsync"," (to validate the token\non incoming requests), never ",[33,624,617],{},[10,626,628],{"id":627},"challenge-vs-forbid-401-vs-403","Challenge vs Forbid — 401 vs 403",[15,630,631],{},"This distinction trips up many candidates.",[633,634,635,651],"table",{},[636,637,638],"thead",{},[639,640,641,645,648],"tr",{},[642,643,644],"th",{},"Situation",[642,646,647],{},"Result",[642,649,650],{},"What happens",[652,653,654,671],"tbody",{},[639,655,656,662,664],{},[657,658,659,660],"td",{},"Request is anonymous and hits ",[33,661,108],{},[657,663,112],{},[657,665,666,667,670],{},"Cookie → redirect to login; JWT → ",[33,668,669],{},"WWW-Authenticate"," header",[639,672,673,676,678],{},[657,674,675],{},"Authenticated user lacks required role",[657,677,116],{},[657,679,680],{},"Cookie → redirect to access denied; JWT → 403 body",[44,682,684],{"className":46,"code":683,"language":48,"meta":49,"style":49},"\u002F\u002F Manual trigger in a controller:\npublic IActionResult GetAdminData()\n{\n    if (!User.Identity!.IsAuthenticated) return Challenge(); \u002F\u002F 401\n    if (!User.IsInRole(\"Admin\"))         return Forbid();    \u002F\u002F 403\n    return Ok(_adminService.GetData());\n}\n",[33,685,686,691,696,700,705,710,715],{"__ignoreMap":49},[53,687,688],{"class":55,"line":56},[53,689,690],{},"\u002F\u002F Manual trigger in a controller:\n",[53,692,693],{"class":55,"line":62},[53,694,695],{},"public IActionResult GetAdminData()\n",[53,697,698],{"class":55,"line":68},[53,699,211],{},[53,701,702],{"class":55,"line":74},[53,703,704],{},"    if (!User.Identity!.IsAuthenticated) return Challenge(); \u002F\u002F 401\n",[53,706,707],{"class":55,"line":80},[53,708,709],{},"    if (!User.IsInRole(\"Admin\"))         return Forbid();    \u002F\u002F 403\n",[53,711,712],{"class":55,"line":87},[53,713,714],{},"    return Ok(_adminService.GetData());\n",[53,716,717],{"class":55,"line":93},[53,718,719],{},"}\n",[15,721,722],{},"For cookie-auth APIs (returning JSON, not HTML), override the default redirect behavior:",[44,724,726],{"className":46,"code":725,"language":48,"meta":49,"style":49},".AddCookie(options =>\n{\n    options.Events.OnRedirectToLogin        = ctx => { ctx.Response.StatusCode = 401; return Task.CompletedTask; };\n    options.Events.OnRedirectToAccessDenied = ctx => { ctx.Response.StatusCode = 403; return Task.CompletedTask; };\n});\n",[33,727,728,733,737,742,747],{"__ignoreMap":49},[53,729,730],{"class":55,"line":56},[53,731,732],{},".AddCookie(options =>\n",[53,734,735],{"class":55,"line":62},[53,736,211],{},[53,738,739],{"class":55,"line":68},[53,740,741],{},"    options.Events.OnRedirectToLogin        = ctx => { ctx.Response.StatusCode = 401; return Task.CompletedTask; };\n",[53,743,744],{"class":55,"line":74},[53,745,746],{},"    options.Events.OnRedirectToAccessDenied = ctx => { ctx.Response.StatusCode = 403; return Task.CompletedTask; };\n",[53,748,749],{"class":55,"line":80},[53,750,751],{},"});\n",[10,753,755],{"id":754},"custom-authentication-handlers","Custom authentication handlers",[15,757,758,759,762],{},"When built-in schemes don't fit (API key, HMAC signature, custom SSO), implement\n",[33,760,761],{},"AuthenticationHandler\u003CTOptions>",":",[44,764,766],{"className":46,"code":765,"language":48,"meta":49,"style":49},"public class ApiKeyAuthHandler : AuthenticationHandler\u003CApiKeyAuthOptions>\n{\n    private readonly IApiKeyStore _store;\n\n    public ApiKeyAuthHandler(\n        IOptionsMonitor\u003CApiKeyAuthOptions> options,\n        ILoggerFactory logger,\n        UrlEncoder encoder,\n        IApiKeyStore store)\n        : base(options, logger, encoder)\n        => _store = store;\n\n    protected override async Task\u003CAuthenticateResult> HandleAuthenticateAsync()\n    {\n        if (!Request.Headers.TryGetValue(Options.HeaderName, out var keyValues))\n            return AuthenticateResult.NoResult(); \u002F\u002F scheme doesn't apply to this request\n\n        var client = await _store.FindByKeyAsync(keyValues.FirstOrDefault());\n        if (client is null)\n            return AuthenticateResult.Fail(\"Invalid API key\");\n\n        var claims    = new[] { new Claim(ClaimTypes.NameIdentifier, client.ClientId) };\n        var identity  = new ClaimsIdentity(claims, Scheme.Name);\n        var principal = new ClaimsPrincipal(identity);\n        var ticket    = new AuthenticationTicket(principal, Scheme.Name);\n\n        return AuthenticateResult.Success(ticket);\n    }\n}\n\nbuilder.Services\n    .AddAuthentication()\n    .AddScheme\u003CApiKeyAuthOptions, ApiKeyAuthHandler>(\"ApiKey\", _ => { });\n",[33,767,768,773,777,782,786,791,796,801,806,811,816,821,825,830,834,839,844,848,853,858,863,867,872,877,882,887,891,896,901,906,911,916,922],{"__ignoreMap":49},[53,769,770],{"class":55,"line":56},[53,771,772],{},"public class ApiKeyAuthHandler : AuthenticationHandler\u003CApiKeyAuthOptions>\n",[53,774,775],{"class":55,"line":62},[53,776,211],{},[53,778,779],{"class":55,"line":68},[53,780,781],{},"    private readonly IApiKeyStore _store;\n",[53,783,784],{"class":55,"line":74},[53,785,84],{"emptyLinePlaceholder":83},[53,787,788],{"class":55,"line":80},[53,789,790],{},"    public ApiKeyAuthHandler(\n",[53,792,793],{"class":55,"line":87},[53,794,795],{},"        IOptionsMonitor\u003CApiKeyAuthOptions> options,\n",[53,797,798],{"class":55,"line":93},[53,799,800],{},"        ILoggerFactory logger,\n",[53,802,803],{"class":55,"line":99},[53,804,805],{},"        UrlEncoder encoder,\n",[53,807,808],{"class":55,"line":179},[53,809,810],{},"        IApiKeyStore store)\n",[53,812,813],{"class":55,"line":185},[53,814,815],{},"        : base(options, logger, encoder)\n",[53,817,818],{"class":55,"line":191},[53,819,820],{},"        => _store = store;\n",[53,822,823],{"class":55,"line":196},[53,824,84],{"emptyLinePlaceholder":83},[53,826,827],{"class":55,"line":202},[53,828,829],{},"    protected override async Task\u003CAuthenticateResult> HandleAuthenticateAsync()\n",[53,831,832],{"class":55,"line":208},[53,833,161],{},[53,835,836],{"class":55,"line":214},[53,837,838],{},"        if (!Request.Headers.TryGetValue(Options.HeaderName, out var keyValues))\n",[53,840,841],{"class":55,"line":220},[53,842,843],{},"            return AuthenticateResult.NoResult(); \u002F\u002F scheme doesn't apply to this request\n",[53,845,846],{"class":55,"line":226},[53,847,84],{"emptyLinePlaceholder":83},[53,849,850],{"class":55,"line":232},[53,851,852],{},"        var client = await _store.FindByKeyAsync(keyValues.FirstOrDefault());\n",[53,854,855],{"class":55,"line":238},[53,856,857],{},"        if (client is null)\n",[53,859,860],{"class":55,"line":244},[53,861,862],{},"            return AuthenticateResult.Fail(\"Invalid API key\");\n",[53,864,865],{"class":55,"line":250},[53,866,84],{"emptyLinePlaceholder":83},[53,868,869],{"class":55,"line":255},[53,870,871],{},"        var claims    = new[] { new Claim(ClaimTypes.NameIdentifier, client.ClientId) };\n",[53,873,874],{"class":55,"line":261},[53,875,876],{},"        var identity  = new ClaimsIdentity(claims, Scheme.Name);\n",[53,878,879],{"class":55,"line":267},[53,880,881],{},"        var principal = new ClaimsPrincipal(identity);\n",[53,883,884],{"class":55,"line":273},[53,885,886],{},"        var ticket    = new AuthenticationTicket(principal, Scheme.Name);\n",[53,888,889],{"class":55,"line":279},[53,890,84],{"emptyLinePlaceholder":83},[53,892,893],{"class":55,"line":284},[53,894,895],{},"        return AuthenticateResult.Success(ticket);\n",[53,897,898],{"class":55,"line":290},[53,899,900],{},"    }\n",[53,902,904],{"class":55,"line":903},29,[53,905,719],{},[53,907,909],{"class":55,"line":908},30,[53,910,84],{"emptyLinePlaceholder":83},[53,912,914],{"class":55,"line":913},31,[53,915,146],{},[53,917,919],{"class":55,"line":918},32,[53,920,921],{},"    .AddAuthentication()\n",[53,923,925],{"class":55,"line":924},33,[53,926,927],{},"    .AddScheme\u003CApiKeyAuthOptions, ApiKeyAuthHandler>(\"ApiKey\", _ => { });\n",[15,929,930,933,934,937],{},[33,931,932],{},"NoResult()"," means \"this scheme doesn't apply\" — the framework tries the next scheme.\n",[33,935,936],{},"Fail()"," means \"this scheme applied but the credential was invalid\" — authentication fails immediately.",[10,939,941],{"id":940},"external-oauth-providers","External OAuth providers",[15,943,944],{},"For Google, Microsoft, or generic OIDC, pair an OAuth scheme with a local cookie scheme:",[44,946,948],{"className":46,"code":947,"language":48,"meta":49,"style":49},"builder.Services\n    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)\n    .AddCookie()\n    .AddGoogle(options =>\n    {\n        options.ClientId     = config[\"Auth:Google:ClientId\"]!;\n        options.ClientSecret = config[\"Auth:Google:ClientSecret\"]!;\n        options.Scope.Add(\"profile\");\n        options.Scope.Add(\"email\");\n    });\n\n\u002F\u002F Trigger the OAuth redirect:\n[HttpGet(\"login\u002Fgoogle\")]\npublic IActionResult LoginGoogle()\n    => Challenge(new AuthenticationProperties { RedirectUri = \"\u002F\" }, \"Google\");\n",[33,949,950,954,958,962,966,970,974,978,983,988,992,996,1001,1006,1011],{"__ignoreMap":49},[53,951,952],{"class":55,"line":56},[53,953,146],{},[53,955,956],{"class":55,"line":62},[53,957,151],{},[53,959,960],{"class":55,"line":68},[53,961,427],{},[53,963,964],{"class":55,"line":74},[53,965,437],{},[53,967,968],{"class":55,"line":80},[53,969,161],{},[53,971,972],{"class":55,"line":87},[53,973,446],{},[53,975,976],{"class":55,"line":93},[53,977,451],{},[53,979,980],{"class":55,"line":99},[53,981,982],{},"        options.Scope.Add(\"profile\");\n",[53,984,985],{"class":55,"line":179},[53,986,987],{},"        options.Scope.Add(\"email\");\n",[53,989,990],{"class":55,"line":185},[53,991,188],{},[53,993,994],{"class":55,"line":191},[53,995,84],{"emptyLinePlaceholder":83},[53,997,998],{"class":55,"line":196},[53,999,1000],{},"\u002F\u002F Trigger the OAuth redirect:\n",[53,1002,1003],{"class":55,"line":202},[53,1004,1005],{},"[HttpGet(\"login\u002Fgoogle\")]\n",[53,1007,1008],{"class":55,"line":208},[53,1009,1010],{},"public IActionResult LoginGoogle()\n",[53,1012,1013],{"class":55,"line":214},[53,1014,1015],{},"    => Challenge(new AuthenticationProperties { RedirectUri = \"\u002F\" }, \"Google\");\n",[15,1017,1018],{},"The OAuth scheme handles the redirect and token exchange; the cookie scheme persists the resulting\nidentity. Without the cookie scheme, every request would re-trigger the OAuth flow.",[10,1020,1022],{"id":1021},"data-protection-and-key-management","Data Protection and key management",[15,1024,1025],{},"Cookie encryption relies on ASP.NET Core Data Protection. For production (multiple machines or\nrestarts), configure a shared persistent key store:",[44,1027,1029],{"className":46,"code":1028,"language":48,"meta":49,"style":49},"\u002F\u002F File system (shared NFS\u002FEFS path):\nbuilder.Services.AddDataProtection()\n    .PersistKeysToFileSystem(new DirectoryInfo(\"\u002Fkeys\"))\n    .SetApplicationName(\"MyApp\");\n\n\u002F\u002F Redis (containerized workloads):\nbuilder.Services.AddDataProtection()\n    .PersistKeysToStackExchangeRedis(redis, \"DataProtection-Keys\");\n",[33,1030,1031,1036,1041,1046,1051,1055,1060,1064],{"__ignoreMap":49},[53,1032,1033],{"class":55,"line":56},[53,1034,1035],{},"\u002F\u002F File system (shared NFS\u002FEFS path):\n",[53,1037,1038],{"class":55,"line":62},[53,1039,1040],{},"builder.Services.AddDataProtection()\n",[53,1042,1043],{"class":55,"line":68},[53,1044,1045],{},"    .PersistKeysToFileSystem(new DirectoryInfo(\"\u002Fkeys\"))\n",[53,1047,1048],{"class":55,"line":74},[53,1049,1050],{},"    .SetApplicationName(\"MyApp\");\n",[53,1052,1053],{"class":55,"line":80},[53,1054,84],{"emptyLinePlaceholder":83},[53,1056,1057],{"class":55,"line":87},[53,1058,1059],{},"\u002F\u002F Redis (containerized workloads):\n",[53,1061,1062],{"class":55,"line":93},[53,1063,1040],{},[53,1065,1066],{"class":55,"line":99},[53,1067,1068],{},"    .PersistKeysToStackExchangeRedis(redis, \"DataProtection-Keys\");\n",[15,1070,1071],{},"Without shared keys, a new app instance can't decrypt cookies set by another instance — users\nget silently logged out on every deploy.",[10,1073,1075],{"id":1074},"minimal-api-authentication","Minimal API authentication",[44,1077,1079],{"className":46,"code":1078,"language":48,"meta":49,"style":49},"\u002F\u002F Single endpoint:\napp.MapGet(\"\u002Fprofile\", (ClaimsPrincipal user) =>\n    Results.Ok(new { Name = user.Identity!.Name })\n).RequireAuthorization();\n\n\u002F\u002F Entire group:\nvar api = app.MapGroup(\"\u002Fapi\").RequireAuthorization();\napi.MapGet(\"\u002Forders\",  (IOrderService svc) => svc.GetAll());\napi.MapGet(\"\u002Fhealth\",  () => Results.Ok()).AllowAnonymous(); \u002F\u002F explicit exception\n",[33,1080,1081,1086,1091,1096,1101,1105,1110,1115,1120],{"__ignoreMap":49},[53,1082,1083],{"class":55,"line":56},[53,1084,1085],{},"\u002F\u002F Single endpoint:\n",[53,1087,1088],{"class":55,"line":62},[53,1089,1090],{},"app.MapGet(\"\u002Fprofile\", (ClaimsPrincipal user) =>\n",[53,1092,1093],{"class":55,"line":68},[53,1094,1095],{},"    Results.Ok(new { Name = user.Identity!.Name })\n",[53,1097,1098],{"class":55,"line":74},[53,1099,1100],{},").RequireAuthorization();\n",[53,1102,1103],{"class":55,"line":80},[53,1104,84],{"emptyLinePlaceholder":83},[53,1106,1107],{"class":55,"line":87},[53,1108,1109],{},"\u002F\u002F Entire group:\n",[53,1111,1112],{"class":55,"line":93},[53,1113,1114],{},"var api = app.MapGroup(\"\u002Fapi\").RequireAuthorization();\n",[53,1116,1117],{"class":55,"line":99},[53,1118,1119],{},"api.MapGet(\"\u002Forders\",  (IOrderService svc) => svc.GetAll());\n",[53,1121,1122],{"class":55,"line":179},[53,1123,1124],{},"api.MapGet(\"\u002Fhealth\",  () => Results.Ok()).AllowAnonymous(); \u002F\u002F explicit exception\n",[10,1126,1128],{"id":1127},"recap","Recap",[15,1130,1131,1132,1134,1135,1137,1138,1141,1142,1145,1146,1149,1150,1152],{},"Authentication in ASP.NET Core is a pipeline: ",[33,1133,483],{}," populates ",[33,1136,130],{},"\nfrom the credential, then ",[33,1139,1140],{},"UseAuthorization"," decides what that user can do. Cookie authentication\nis the default for web apps; JWT bearer is standard for APIs. Claims carry identity data as\nkey-value pairs — keep them minimal and non-sensitive. Use ",[33,1143,1144],{},"Challenge"," for anonymous requests\n(401) and ",[33,1147,1148],{},"Forbid"," for authenticated but unauthorized ones (403). For custom protocols, implement\n",[33,1151,761],{},". In production, configure a shared Data Protection key store to\nsurvive restarts and scale-out.",[1154,1155,1156],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":49,"searchDepth":62,"depth":62,"links":1158},[1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171],{"id":12,"depth":62,"text":13},{"id":20,"depth":62,"text":21},{"id":120,"depth":62,"text":121},{"id":299,"depth":62,"text":300},{"id":383,"depth":62,"text":384},{"id":477,"depth":62,"text":478},{"id":531,"depth":62,"text":532},{"id":627,"depth":62,"text":628},{"id":754,"depth":62,"text":755},{"id":940,"depth":62,"text":941},{"id":1021,"depth":62,"text":1022},{"id":1074,"depth":62,"text":1075},{"id":1127,"depth":62,"text":1128},"How ASP.NET Core authentication works end-to-end — cookie auth, ClaimsPrincipal, multiple schemes, custom handlers, and the challenge vs forbid distinction that trips up most developers.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-authentication","\u002Fdotnet\u002Fsecurity\u002Fauthentication",{"title":5,"description":1172},"blog\u002Fdotnet-authentication","Security","security","2026-06-23","Rae5tdxf1A3g2hycpZV7XvfOO1UNrm35Yz0MBwycY4E",1782244083913]