[{"data":1,"prerenderedAt":1075},["ShallowReactive",2],{"blog-post-\u002Fblog\u002Faspnet-core-authentication-endpoints":3,"blog-surround-\u002Fblog\u002Faspnet-core-authentication-endpoints":1064},{"id":4,"title":5,"authors":6,"badge":12,"body":14,"date":1045,"description":1046,"extension":1047,"image":1048,"meta":1049,"navigation":318,"path":1057,"seo":1058,"stem":1062,"__hash__":1063},"posts\u002Fblog\u002Faspnet-core-authentication-endpoints.md","ASP.NET Core Authentication Endpoints: Why I Built AuthEndpoints",[7],{"name":8,"to":9,"avatar":10},"Made Y","\u002F",{"src":11},"https:\u002F\u002Fmedia.harten.id\u002Fimages\u002Fmadeyoga-avatar.webp",{"label":13},"Open Source",{"type":15,"value":16,"toc":1037},"minimark",[17,41,44,49,68,71,78,95,101,108,114,117,120,123,127,133,136,139,153,157,166,233,240,799,810,824,849,853,860,868,882,888,894,907,913,929,933,939,952,955,981,990,994,997,1030,1033],[18,19,20,24,25,24,28,24,31,24,34,24,37,40],"p",{},[21,22,23],"code",{},"AddAuthentication",". ",[21,26,27],{},"AddAuthorization",[21,29,30],{},"AddCookie",[21,32,33],{},"AddJwtBearer",[21,35,36],{},"UseAuthentication",[21,38,39],{},"UseAuthorization",".",[18,42,43],{},"In 2022 I started using ASP.NET Core for new projects. I came from Django, where you create a project and auth is already there: a user model, login, logout, password reset, and an admin. However, in aspnetcore I wrote those six lines, ran the app, and had nowhere to log in. There were no aspnetcore authentication endpoints, only the plumbing for them.",[45,46,48],"h2",{"id":47},"the-problem-no-endpoints","The problem: no endpoints",[18,50,51,52,55,56,59,60,63,64,67],{},"It took me a while to understand why. I read the aspnetcore source and a lot of blog samples. The ",[21,53,54],{},"Add*"," and ",[21,57,58],{},"Use*"," calls register the auth services and middleware. They don't give you a single endpoint. Register, login, refresh, 2FA and password reset are all yours to write. To write them, you first have to learn ",[21,61,62],{},"UserManager",", ",[21,65,66],{},"SignInManager",", token providers and authentication schemes. That's a lot for a beginner. ASP.NET Core is unopinionated on purpose, and auth is where you feel it most.",[18,69,70],{},"So I wrote the endpoints once and turned them into a library. I started AuthEndpoints in April 2022. I've used it ever since in every client project, including POS systems, CRMs and internal apps.",[18,72,73],{},[74,75],"img",{"alt":76,"src":77},"David Fowler forked AuthEndpoints","https:\u002F\u002Fdev-to-uploads.s3.us-east-2.amazonaws.com\u002Fuploads\u002Farticles\u002Feqxf4ut8ytbcp3hub50i.png",[18,79,80,81,88,89,94],{},"That July the repo got a fork from ",[82,83,87],"a",{"href":84,"rel":85},"https:\u002F\u002Fgithub.com\u002Fdavidfowl",[86],"nofollow","David Fowler",", one of the engineers behind aspnetcore. Stars started coming in. When I looked into why, I found out it wasn't just me. A month earlier someone had opened ",[82,90,93],{"href":91,"rel":92},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Faspnetcore\u002Fissues\u002F42158",[86],"dotnet\u002Faspnetcore#42158",", asking for simple auth options in the SPA templates. The thread grew into a long back-and-forth discussions.",[18,96,97],{},[74,98],{"alt":99,"src":100},"ASP.NET Core auth discussions","https:\u002F\u002Fdev-to-uploads.s3.us-east-2.amazonaws.com\u002Fuploads\u002Farticles\u002Fk6if70zn83s3podtx1hm.png",[18,102,103,104,107],{},"The opening post quoted developers saying \"I'm learning authentication by trial-and-error\" and called it frightening. A consultant wrote that a team new to .NET couldn't get auth working for a month and gave up for a while. Others pointed to Laravel, where this part just works. David Fowler was replying in that thread too. At the time, aspnetcore had no built-in auth endpoints at all. ",[21,105,106],{},"MapIdentityApi"," didn't come until .NET 8, in November 2023.",[18,109,110,111,113],{},"The more I learned about auth (while building client projects), the more I saw how opinionated it is. Cookies or tokens. Passwords, email codes, passkeys, social logins, oauth, oidc, step ups, two factor authentication. Every app picks a different mix depending on their needs. I also think maybe that's why aspnetcore left the endpoints out. A framework that stays unopinionated can't pick one for you. Even with ",[21,112,106],{},", it is, in my opinion an opinionated solution that can't fit on every apps out there.",[18,115,116],{},"That gave me the idea for AuthEndpoints: a set of minimal API endpoints where you pick the ones you need. However, the execution was really bad. I didn't have the experience yet, so the early versions gave you no real choice. You got one opinionated way of doing auth, password auth, and that was it.",[18,118,119],{},"Later, AI tools helped me reshape the idea and the code, so each piece stands on its own. You pick your sign-in method, your session type and the extras you want, and map only those. The idea became composable authentication.",[18,121,122],{},"AuthEndpoints 4 is the result. The version I want other developers to try. It is free and MIT licensed. The main goal is to simplify aspnetcore authentication. Whether you're new to aspnetcore or have used it for years, please give it a try!",[45,124,126],{"id":125},"authendpoints-4-an-aspnet-core-auth-library-on-top-of-identity","AuthEndpoints 4: an ASP.NET Core auth library on top of Identity",[18,128,129],{},[74,130],{"alt":131,"src":132},"AuthEndpoints","https:\u002F\u002Fdev-to-uploads.s3.us-east-2.amazonaws.com\u002Fuploads\u002Farticles\u002Ftg7c759evwsat58yei5a.png",[18,134,135],{},"AuthEndpoints is an aspnetcore auth library. It doesn't replace aspnetcore Identity. Identity still owns your users, password hashing and lockout. AuthEndpoints maps the HTTP endpoints on top of it. Opinionated recipes ready for different auth types, while remain flexible, allowing users to compose their own auth stack.",[18,137,138],{},"Version 4.0.0 shipped on 15 October 2026. Two patch releases followed, and 4.0.2 is the current one on NuGet. It targets .NET 10 and uses EF Core for the user store.",[18,140,141,142,55,145,148,149,152],{},"The big change in 4 is a new entry point for new projects, ",[21,143,144],{},"AddPasswordAuth",[21,146,147],{},"MapPasswordAuth",". It is a Password auth recipe (or I call it facade). One pair of calls gives you password sign-up, sign-in, recovery, email confirmation, account management and one session, all under ",[21,150,151],{},"\u002Fauth",". Cookie is the default session. Jwt is the other one.",[45,154,156],{"id":155},"quick-start","Quick start",[18,158,159,160,165],{},"This comes from the ",[82,161,164],{"href":162,"rel":163},"https:\u002F\u002Fauthendpoints.harten.id\u002Fgetting-started\u002Fquick-start\u002F",[86],"quick start"," in the docs. You need the .NET 10 SDK.",[167,168,173],"pre",{"className":169,"code":170,"language":171,"meta":172,"style":172},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark monokai","dotnet new web -n AuthDemo\ncd AuthDemo\ndotnet add package AuthEndpoints\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\n","bash","",[21,174,175,198,207,221],{"__ignoreMap":172},[176,177,180,184,188,191,195],"span",{"class":178,"line":179},"line",1,[176,181,183],{"class":182},"sR7ES","dotnet",[176,185,187],{"class":186},"sLACW"," new",[176,189,190],{"class":186}," web",[176,192,194],{"class":193},"sFhLe"," -n",[176,196,197],{"class":186}," AuthDemo\n",[176,199,201,205],{"class":178,"line":200},2,[176,202,204],{"class":203},"sMLJd","cd",[176,206,197],{"class":186},[176,208,210,212,215,218],{"class":178,"line":209},3,[176,211,183],{"class":182},[176,213,214],{"class":186}," add",[176,216,217],{"class":186}," package",[176,219,220],{"class":186}," AuthEndpoints\n",[176,222,224,226,228,230],{"class":178,"line":223},4,[176,225,183],{"class":182},[176,227,214],{"class":186},[176,229,217],{"class":186},[176,231,232],{"class":186}," Microsoft.EntityFrameworkCore.Sqlite\n",[18,234,235,236,239],{},"Then ",[21,237,238],{},"Program.cs",":",[167,241,245],{"className":242,"code":243,"language":244,"meta":172,"style":172},"language-csharp shiki shiki-themes material-theme-lighter github-light github-dark monokai","using AuthEndpoints;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Identity.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddDbContext\u003CAppDbContext>(o => o.UseSqlite(\"Data Source=app.db\"));\nbuilder.Services.AddPasswordAuth\u003CAppUser, AppDbContext>();\nbuilder.Services.AddTransient\u003CIEmailSender\u003CAppUser>, ConsoleEmailSender>();\n\nvar app = builder.Build();\n\nusing (var scope = app.Services.CreateScope())\n{\n    scope.ServiceProvider.GetRequiredService\u003CAppDbContext>().Database.EnsureCreated();\n}\n\napp.UseAuthEndpoints();\napp.MapPasswordAuth\u003CAppUser>();\n\napp.MapGet(\"\u002Fme\", (System.Security.Claims.ClaimsPrincipal user) => new { email = user.Identity!.Name })\n    .RequireAuthorization();\n\napp.Run();\n\npublic class AppUser : IdentityUser { }\n\npublic class AppDbContext(DbContextOptions\u003CAppDbContext> options) : IdentityDbContext\u003CAppUser>(options) { }\n","csharp",[21,246,247,261,280,301,313,320,354,359,413,440,471,476,496,501,531,537,570,576,581,594,609,614,691,702,707,719,724,748,753],{"__ignoreMap":172},[176,248,249,253,257],{"class":178,"line":179},[176,250,252],{"class":251},"shWJe","using",[176,254,256],{"class":255},"sM5v9"," AuthEndpoints",[176,258,260],{"class":259},"swvn1",";\n",[176,262,263,265,268,270,273,275,278],{"class":178,"line":200},[176,264,252],{"class":251},[176,266,267],{"class":255}," Microsoft",[176,269,40],{"class":259},[176,271,272],{"class":255},"AspNetCore",[176,274,40],{"class":259},[176,276,277],{"class":255},"Identity",[176,279,260],{"class":259},[176,281,282,284,286,288,290,292,294,296,299],{"class":178,"line":209},[176,283,252],{"class":251},[176,285,267],{"class":255},[176,287,40],{"class":259},[176,289,272],{"class":255},[176,291,40],{"class":259},[176,293,277],{"class":255},[176,295,40],{"class":259},[176,297,298],{"class":255},"EntityFrameworkCore",[176,300,260],{"class":259},[176,302,303,305,307,309,311],{"class":178,"line":223},[176,304,252],{"class":251},[176,306,267],{"class":255},[176,308,40],{"class":259},[176,310,298],{"class":255},[176,312,260],{"class":259},[176,314,316],{"class":178,"line":315},5,[176,317,319],{"emptyLinePlaceholder":318},true,"\n",[176,321,323,327,331,335,339,341,345,348,351],{"class":178,"line":322},6,[176,324,326],{"class":325},"sQHqT","var",[176,328,330],{"class":329},"sQqfL"," builder",[176,332,334],{"class":333},"sGXK2"," =",[176,336,338],{"class":337},"ss--_"," WebApplication",[176,340,40],{"class":259},[176,342,344],{"class":343},"sD0ED","CreateBuilder",[176,346,347],{"class":259},"(",[176,349,350],{"class":337},"args",[176,352,353],{"class":259},");\n",[176,355,357],{"class":178,"line":356},7,[176,358,319],{"emptyLinePlaceholder":318},[176,360,362,365,367,370,372,375,378,382,385,388,391,394,396,399,401,405,408,410],{"class":178,"line":361},8,[176,363,364],{"class":337},"builder",[176,366,40],{"class":259},[176,368,369],{"class":337},"Services",[176,371,40],{"class":259},[176,373,374],{"class":343},"AddDbContext",[176,376,377],{"class":259},"\u003C",[176,379,381],{"class":380},"sKvfc","AppDbContext",[176,383,384],{"class":259},">(",[176,386,387],{"class":329},"o",[176,389,390],{"class":333}," =>",[176,392,393],{"class":337}," o",[176,395,40],{"class":259},[176,397,398],{"class":343},"UseSqlite",[176,400,347],{"class":259},[176,402,404],{"class":403},"siCPE","\"",[176,406,407],{"class":186},"Data Source=app.db",[176,409,404],{"class":403},[176,411,412],{"class":259},"));\n",[176,414,416,418,420,422,424,426,428,431,434,437],{"class":178,"line":415},9,[176,417,364],{"class":337},[176,419,40],{"class":259},[176,421,369],{"class":337},[176,423,40],{"class":259},[176,425,144],{"class":343},[176,427,377],{"class":259},[176,429,430],{"class":380},"AppUser",[176,432,433],{"class":259},",",[176,435,436],{"class":380}," AppDbContext",[176,438,439],{"class":259},">();\n",[176,441,443,445,447,449,451,454,456,459,461,463,466,469],{"class":178,"line":442},10,[176,444,364],{"class":337},[176,446,40],{"class":259},[176,448,369],{"class":337},[176,450,40],{"class":259},[176,452,453],{"class":343},"AddTransient",[176,455,377],{"class":259},[176,457,458],{"class":380},"IEmailSender",[176,460,377],{"class":259},[176,462,430],{"class":380},[176,464,465],{"class":259},">,",[176,467,468],{"class":380}," ConsoleEmailSender",[176,470,439],{"class":259},[176,472,474],{"class":178,"line":473},11,[176,475,319],{"emptyLinePlaceholder":318},[176,477,479,481,484,486,488,490,493],{"class":178,"line":478},12,[176,480,326],{"class":325},[176,482,483],{"class":329}," app",[176,485,334],{"class":333},[176,487,330],{"class":337},[176,489,40],{"class":259},[176,491,492],{"class":343},"Build",[176,494,495],{"class":259},"();\n",[176,497,499],{"class":178,"line":498},13,[176,500,319],{"emptyLinePlaceholder":318},[176,502,504,507,510,512,515,517,519,521,523,525,528],{"class":178,"line":503},14,[176,505,252],{"class":506},"sRxSC",[176,508,509],{"class":259}," (",[176,511,326],{"class":325},[176,513,514],{"class":329}," scope",[176,516,334],{"class":333},[176,518,483],{"class":337},[176,520,40],{"class":259},[176,522,369],{"class":337},[176,524,40],{"class":259},[176,526,527],{"class":343},"CreateScope",[176,529,530],{"class":259},"())\n",[176,532,534],{"class":178,"line":533},15,[176,535,536],{"class":259},"{\n",[176,538,540,543,545,548,550,553,555,557,560,563,565,568],{"class":178,"line":539},16,[176,541,542],{"class":337},"    scope",[176,544,40],{"class":259},[176,546,547],{"class":337},"ServiceProvider",[176,549,40],{"class":259},[176,551,552],{"class":343},"GetRequiredService",[176,554,377],{"class":259},[176,556,381],{"class":380},[176,558,559],{"class":259},">().",[176,561,562],{"class":337},"Database",[176,564,40],{"class":259},[176,566,567],{"class":343},"EnsureCreated",[176,569,495],{"class":259},[176,571,573],{"class":178,"line":572},17,[176,574,575],{"class":259},"}\n",[176,577,579],{"class":178,"line":578},18,[176,580,319],{"emptyLinePlaceholder":318},[176,582,584,587,589,592],{"class":178,"line":583},19,[176,585,586],{"class":337},"app",[176,588,40],{"class":259},[176,590,591],{"class":343},"UseAuthEndpoints",[176,593,495],{"class":259},[176,595,597,599,601,603,605,607],{"class":178,"line":596},20,[176,598,586],{"class":337},[176,600,40],{"class":259},[176,602,147],{"class":343},[176,604,377],{"class":259},[176,606,430],{"class":380},[176,608,439],{"class":259},[176,610,612],{"class":178,"line":611},21,[176,613,319],{"emptyLinePlaceholder":318},[176,615,617,619,621,624,626,628,631,633,635,637,640,642,645,647,650,652,655,658,661,663,665,668,671,674,676,678,680,683,685,688],{"class":178,"line":616},22,[176,618,586],{"class":337},[176,620,40],{"class":259},[176,622,623],{"class":343},"MapGet",[176,625,347],{"class":259},[176,627,404],{"class":403},[176,629,630],{"class":186},"\u002Fme",[176,632,404],{"class":403},[176,634,433],{"class":259},[176,636,509],{"class":259},[176,638,639],{"class":380},"System",[176,641,40],{"class":259},[176,643,644],{"class":380},"Security",[176,646,40],{"class":259},[176,648,649],{"class":380},"Claims",[176,651,40],{"class":259},[176,653,654],{"class":380},"ClaimsPrincipal",[176,656,657],{"class":329}," user",[176,659,660],{"class":259},")",[176,662,390],{"class":333},[176,664,187],{"class":333},[176,666,667],{"class":259}," {",[176,669,670],{"class":337}," email ",[176,672,673],{"class":333},"=",[176,675,657],{"class":337},[176,677,40],{"class":259},[176,679,277],{"class":337},[176,681,682],{"class":333},"!",[176,684,40],{"class":259},[176,686,687],{"class":337},"Name ",[176,689,690],{"class":259},"})\n",[176,692,694,697,700],{"class":178,"line":693},23,[176,695,696],{"class":259},"    .",[176,698,699],{"class":343},"RequireAuthorization",[176,701,495],{"class":259},[176,703,705],{"class":178,"line":704},24,[176,706,319],{"emptyLinePlaceholder":318},[176,708,710,712,714,717],{"class":178,"line":709},25,[176,711,586],{"class":337},[176,713,40],{"class":259},[176,715,716],{"class":343},"Run",[176,718,495],{"class":259},[176,720,722],{"class":178,"line":721},26,[176,723,319],{"emptyLinePlaceholder":318},[176,725,727,731,734,737,740,743,745],{"class":178,"line":726},27,[176,728,730],{"class":729},"sTNss","public",[176,732,733],{"class":325}," class",[176,735,736],{"class":380}," AppUser",[176,738,739],{"class":259}," :",[176,741,742],{"class":380}," IdentityUser",[176,744,667],{"class":259},[176,746,747],{"class":259}," }\n",[176,749,751],{"class":178,"line":750},28,[176,752,319],{"emptyLinePlaceholder":318},[176,754,756,758,760,762,764,767,769,771,774,777,779,781,784,786,788,790,793,795,797],{"class":178,"line":755},29,[176,757,730],{"class":729},[176,759,733],{"class":325},[176,761,436],{"class":380},[176,763,347],{"class":259},[176,765,766],{"class":380},"DbContextOptions",[176,768,377],{"class":259},[176,770,381],{"class":380},[176,772,773],{"class":259},">",[176,775,776],{"class":329}," options",[176,778,660],{"class":259},[176,780,739],{"class":259},[176,782,783],{"class":380}," IdentityDbContext",[176,785,377],{"class":259},[176,787,430],{"class":380},[176,789,384],{"class":259},[176,791,792],{"class":337},"options",[176,794,660],{"class":259},[176,796,667],{"class":259},[176,798,747],{"class":259},[18,800,801,802,805,806,809],{},"The quick start also has a ",[21,803,804],{},"ConsoleEmailSender"," that writes confirmation and reset links to the log. I left it out here to keep the sample short. You need some ",[21,807,808],{},"IEmailSender\u003CAppUser>"," for the app to send those links.",[18,811,812,813,815,816,818,819,821,822,40],{},"Three calls do the work. ",[21,814,144],{}," registers Identity, the password endpoints, account management and the cookie session. ",[21,817,591],{}," adds authentication, authorization, rate limiting and antiforgery middleware. ",[21,820,147],{}," maps the routes under ",[21,823,151],{},[18,825,826,827,63,830,63,833,63,836,63,839,63,842,63,845,848],{},"Run it, and you have ",[21,828,829],{},"POST \u002Fauth\u002Fregister",[21,831,832],{},"POST \u002Fauth\u002Flogin",[21,834,835],{},"POST \u002Fauth\u002Flogout",[21,837,838],{},"GET \u002Fauth\u002FconfirmEmail",[21,840,841],{},"POST \u002Fauth\u002FforgotPassword",[21,843,844],{},"POST \u002Fauth\u002FresetPassword",[21,846,847],{},"\u002Fauth\u002Fmanage\u002F2fa"," and more. Accounts need a confirmed email before they can log in. That's the default.",[45,850,852],{"id":851},"what-you-get","What you get",[18,854,855,859],{},[856,857,858],"strong",{},"Sessions."," Cookie sessions for browser apps. JWT with a rotating refresh token for clients that need a bearer token. Identity bearer tokens for mobile apps, on the 3.x facade. In 4, the JWT refresh token can also come back in the JSON body for native apps and CLIs that have no cookie. Cookie transport stays the default.",[18,861,862,865,866,40],{},[856,863,864],{},"2FA."," Authenticator codes and recovery codes through ",[21,867,847],{},[18,869,870,873,874,877,878,881],{},[856,871,872],{},"Passkeys."," Set ",[21,875,876],{},"Passkeys.ServerDomain"," and the password facade maps passkey sign-in and passkey management under ",[21,879,880],{},"\u002Fauth\u002Fpasskeys",". Users can add, rename and remove them.",[18,883,884,887],{},[856,885,886],{},"Step-up (ReAuth)."," Before a sensitive change, the user proves who they are again with a password, an authenticator code, a recovery code, a passkey, or an email code. You can require it on your own routes too.",[18,889,890,893],{},[856,891,892],{},"Other sign-in options."," Emailed one-time codes, plus GitHub and Google sign-in (currently plain oauth flow, not oidc).",[18,895,896,899,900,903,904,906],{},[856,897,898],{},"Protection."," Rate limits, CSRF checks on cookie routes that change state, lockout, and checks at startup. Every ",[21,901,902],{},"Map*"," call throws at startup when its ",[21,905,54],{}," call is missing, so a broken setup fails before the first request.",[18,908,909,912],{},[856,910,911],{},"Modules."," If the facade maps more than you want, you can compose smaller modules and map only the routes you need on your own prefixes.",[18,914,915,916,55,919,922,923,928],{},"If you already use the 3.x ",[21,917,918],{},"AddAuthEndpoints",[21,920,921],{},"MapAuthEndpoints",", they still work in 4. Some setups need a database migration, and a few smaller APIs changed. Read the ",[82,924,927],{"href":925,"rel":926},"https:\u002F\u002Fauthendpoints.harten.id\u002Fgetting-started\u002Fupgrading-to-4\u002F",[86],"upgrade guide"," before you bump the version.",[45,930,932],{"id":931},"authendpoints-vs-mapidentityapi","AuthEndpoints vs MapIdentityApi",[18,934,935,936,938],{},"If you're looking for a ",[21,937,106],{}," alternative, this is the honest comparison in dotnet10.",[18,940,941,947,948,951],{},[82,942,945],{"href":943,"rel":944},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Faspnet\u002Fcore\u002Fsecurity\u002Fauthentication\u002Fidentity-api-authorization",[86],[21,946,106],{}," maps ten routes: register, login, refresh, confirm email, resend confirmation, forgot password, reset password, 2FA, and get or update account info. Login returns Identity bearer tokens by default, or a cookie if you pass ",[21,949,950],{},"useCookies",". Microsoft's docs say those tokens are not standard JWTs. It's a good start, and both libraries use the same Identity user store.",[18,953,954],{},"Here is what it doesn't do:",[956,957,958,962,965,968,971,978],"ul",{},[959,960,961],"li",{},"It maps no logout route. The Microsoft docs tell you to write your own.",[959,963,964],{},"It has no passkey endpoints.",[959,966,967],{},"It maps no CSRF token route for cookie clients.",[959,969,970],{},"It has no step-up check for host endpoints.",[959,972,973,974,977],{},"You can't leave out routes like ",[21,975,976],{},"\u002Fregister",". It maps all ten.",[959,979,980],{},"Rate limiting, antiforgery and CORS are yours to wire up.",[18,982,983,984,989],{},"The docs have a ",[82,985,988],{"href":986,"rel":987},"https:\u002F\u002Fauthendpoints.harten.id\u002Fconcepts\u002Fcompare\u002F",[86],"full comparison"," with the routes side by side.",[45,991,993],{"id":992},"try-it","Try it",[18,995,996],{},"I built AuthEndpoints to simplify authentication in ASP.NET Core web apis. Four years later it runs in every project I ship.",[956,998,999,1007,1014,1022],{},[959,1000,1001,1002],{},"Docs: ",[82,1003,1006],{"href":1004,"rel":1005},"https:\u002F\u002Fauthendpoints.harten.id\u002F",[86],"authendpoints.harten.id",[959,1008,1009,1010],{},"Quick start: ",[82,1011,1013],{"href":162,"rel":1012},[86],"authendpoints.harten.id\u002Fgetting-started\u002Fquick-start",[959,1015,1016,1017],{},"NuGet: ",[82,1018,1021],{"href":1019,"rel":1020},"https:\u002F\u002Fwww.nuget.org\u002Fpackages\u002FAuthEndpoints",[86],"nuget.org\u002Fpackages\u002FAuthEndpoints",[959,1023,1024,1025],{},"GitHub: ",[82,1026,1029],{"href":1027,"rel":1028},"https:\u002F\u002Fgithub.com\u002Fmadeyoga\u002FAuthEndpoints",[86],"github.com\u002Fmadeyoga\u002FAuthEndpoints",[18,1031,1032],{},"If something breaks or a doc is unclear, please open an issue and let me know!",[1034,1035,1036],"style",{},"html pre.shiki code .sR7ES, html code.shiki .sR7ES{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sFhLe, html code.shiki .sFhLe{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sMLJd, html code.shiki .sMLJd{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#66D9EF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .shWJe, html code.shiki .shWJe{--shiki-light:#F76D47;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .sM5v9, html code.shiki .sM5v9{--shiki-light:#90A4AE;--shiki-light-text-decoration:inherit;--shiki-default:#6F42C1;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-text-decoration:underline}html pre.shiki code .swvn1, html code.shiki .swvn1{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sQHqT, html code.shiki .sQHqT{--shiki-light:#E2931D;--shiki-light-font-style:inherit;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sQqfL, html code.shiki .sQqfL{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#F8F8F2}html pre.shiki code .sGXK2, html code.shiki .sGXK2{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .ss--_, html code.shiki .ss--_{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sD0ED, html code.shiki .sD0ED{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .sKvfc, html code.shiki .sKvfc{--shiki-light:#E2931D;--shiki-light-text-decoration:inherit;--shiki-default:#6F42C1;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-text-decoration:underline}html pre.shiki code .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sRxSC, html code.shiki .sRxSC{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#F92672;--shiki-sepia-font-style:inherit}html pre.shiki code .sTNss, html code.shiki .sTNss{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}",{"title":172,"searchDepth":200,"depth":209,"links":1038},[1039,1040,1041,1042,1043,1044],{"id":47,"depth":200,"text":48},{"id":125,"depth":200,"text":126},{"id":155,"depth":200,"text":156},{"id":851,"depth":200,"text":852},{"id":931,"depth":200,"text":932},{"id":992,"depth":200,"text":993},"2026-10-22","In 2022 I came to ASP.NET Core from Django and found auth middleware but no endpoints to log in. So I built AuthEndpoints. Version 4 makes it composable.","md",null,{"sitemap":1050,"schemaOrg":1051},{"lastmod":1045},[1052],{"type":1053,"headline":5,"author":1054,"datePublished":1045},"BlogPosting",{"type":1055,"name":1056},"Person","Made Yoga","\u002Fblog\u002Faspnet-core-authentication-endpoints",{"title":1059,"description":1060,"keywords":1061},"ASP.NET Core Auth Endpoints: Why I Built AuthEndpoints","ASP.NET Core wires up auth middleware but no login, register, refresh or 2FA endpoints. AuthEndpoints 4 adds them with cookies, JWT and passkeys.","dotnet, aspnetcore, authentication, csharp","blog\u002Faspnet-core-authentication-endpoints","5ZxVoeCg0xb0U2YROZwWgBjCLWwger91XmL5NIx3Fow",[1065,1070],{"title":1066,"path":1067,"stem":1068,"description":1069},"Experimenting with Hermes Agent + DeepSeek-V4-Flash-0731 for ASP.NET Core and Nuxt","\u002Fblog\u002Fai-agent-workflow-deepseek-hermes","blog\u002Fai-agent-workflow-deepseek-hermes","Lab notes on running Hermes Agent with DeepSeek-V4-Flash-0731 beta, teaching .NET and Nuxt via skills, wiring Nuxt and Nuxt UI MCPs, and what still feels rough.",{"title":1071,"path":1072,"stem":1073,"description":1074},"AuthEndpoints 3.0 RC: a composable auth endpoints library for ASP.NET Core","\u002Fblog\u002Fauthendpoints-3-0-rc","blog\u002Fauthendpoints-3-0-rc","Announcing AuthEndpoints 3.0 RC, an auth library for ASP.NET Core. Composable auth endpoints on top of ASP.NET Core Identity: registration, login, 2FA, password reset, and passkeys bootstrapped in minutes.",1792662844396]