[{"data":1,"prerenderedAt":1306},["ShallowReactive",2],{"blog-post-\u002Fblog\u002Fmapidentityapi-alternative":3,"blog-surround-\u002Fblog\u002Fmapidentityapi-alternative":1219,"blog-author-profile":1225},{"id":4,"title":5,"authors":6,"badge":12,"body":14,"date":1202,"description":1203,"extension":1204,"image":1205,"meta":1206,"navigation":108,"path":1214,"seo":1215,"stem":1217,"__hash__":1218},"posts\u002Fblog\u002Fmapidentityapi-alternative.md","MapIdentityApi Alternative for ASP.NET Core: The Gaps I Hit",[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":1186},"minimark",[17,29,32,37,45,170,207,210,324,328,333,336,496,502,517,521,533,538,601,627,631,654,659,828,855,859,869,877,881,887,898,902,911,922,926,943,959,963,1053,1061,1065,1068,1071,1075,1078,1081,1084,1106,1115,1119,1125,1138,1146,1155,1164,1173,1182],[18,19,20,21,28],"p",{},"MapIdentityApi is a good start, and for many apps it's enough. In my client projects, though, I kept running into the same few gaps. If you're looking for a MapIdentityApi alternative, you may have hit some of them too. Those gaps are why I built AuthEndpoints. This post walks through each one with a short example and shows how AuthEndpoints 4 handles it. The backstory is in ",[22,23,27],"a",{"href":24,"rel":25},"https:\u002F\u002Fmadeyoga.harten.id\u002Fblog\u002Faspnet-core-authentication-endpoints\u002F",[26],"nofollow","Why I built AuthEndpoints",".",[18,30,31],{},"The more client projects I built, the more I saw how different auth looks from app to app. A POS in a shop wants cookie sessions and staff logins. A mobile app wants tokens it can refresh. A CRM might need 2FA, passkeys or Google sign-in. The right setup depends on what the app needs, not on one default. MapIdentityApi gives you one fixed set of routes, and that set doesn't always match.",[33,34,36],"h2",{"id":35},"mapidentityapi-maps-ten-routes-in-one-call","MapIdentityApi maps ten routes in one call",[18,38,39,44],{},[22,40,43],{"href":41,"rel":42},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Faspnet\u002Fcore\u002Fsecurity\u002Fauthentication\u002Fidentity-api-authorization",[26],"MapIdentityApi"," came with .NET 8. One call maps ten routes on your Identity user store:",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-csharp shiki shiki-themes material-theme-lighter github-light github-dark monokai","builder.Services.AddIdentityApiEndpoints\u003CAppUser>()\n    .AddEntityFrameworkStores\u003CAppDbContext>();\n\nvar app = builder.Build();\napp.MapGroup(\"\u002Fidentity\").MapIdentityApi\u003CAppUser>();\n","csharp","",[53,54,55,86,103,109,135],"code",{"__ignoreMap":51},[56,57,60,64,67,70,72,76,79,83],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"ss--_","builder",[56,65,28],{"class":66},"swvn1",[56,68,69],{"class":62},"Services",[56,71,28],{"class":66},[56,73,75],{"class":74},"sD0ED","AddIdentityApiEndpoints",[56,77,78],{"class":66},"\u003C",[56,80,82],{"class":81},"sKvfc","AppUser",[56,84,85],{"class":66},">()",[56,87,89,92,95,97,100],{"class":58,"line":88},2,[56,90,91],{"class":66},"    .",[56,93,94],{"class":74},"AddEntityFrameworkStores",[56,96,78],{"class":66},[56,98,99],{"class":81},"AppDbContext",[56,101,102],{"class":66},">();",[56,104,106],{"class":58,"line":105},3,[56,107,51],{"emptyLinePlaceholder":108},true,[56,110,112,116,120,124,127,129,132],{"class":58,"line":111},4,[56,113,115],{"class":114},"sQHqT","var",[56,117,119],{"class":118},"sQqfL"," app",[56,121,123],{"class":122},"sGXK2"," =",[56,125,126],{"class":62}," builder",[56,128,28],{"class":66},[56,130,131],{"class":74},"Build",[56,133,134],{"class":66},"();",[56,136,138,141,143,146,149,153,157,159,162,164,166,168],{"class":58,"line":137},5,[56,139,140],{"class":62},"app",[56,142,28],{"class":66},[56,144,145],{"class":74},"MapGroup",[56,147,148],{"class":66},"(",[56,150,152],{"class":151},"siCPE","\"",[56,154,156],{"class":155},"sLACW","\u002Fidentity",[56,158,152],{"class":151},[56,160,161],{"class":66},").",[56,163,43],{"class":74},[56,165,78],{"class":66},[56,167,82],{"class":81},[56,169,102],{"class":66},[18,171,172,173,176,177,176,180,176,183,176,186,176,189,176,192,176,195,198,199,202,203,206],{},"In .NET 10 you get ",[53,174,175],{},"\u002Fregister",", ",[53,178,179],{},"\u002Flogin",[53,181,182],{},"\u002Frefresh",[53,184,185],{},"\u002FconfirmEmail",[53,187,188],{},"\u002FresendConfirmationEmail",[53,190,191],{},"\u002FforgotPassword",[53,193,194],{},"\u002FresetPassword",[53,196,197],{},"\u002Fmanage\u002F2fa",", and ",[53,200,201],{},"GET \u002Fmanage\u002Finfo"," and ",[53,204,205],{},"POST \u002Fmanage\u002Finfo",". You add no package and write no handlers. The gaps below are where it stopped working for me.",[18,208,209],{},"AuthEndpoints 4 sits on the same Identity and EF Core store and targets .NET 10. The basic setup looks like this:",[46,211,213],{"className":48,"code":212,"language":50,"meta":51,"style":51},"builder.Services.AddPasswordAuth\u003CAppUser, AppDbContext>();\nbuilder.Services.AddTransient\u003CIEmailSender\u003CAppUser>, MyEmailSender>();\n\nvar app = builder.Build();\napp.UseAuthEndpoints();          \u002F\u002F authentication, authorization, rate limiting, antiforgery\napp.MapPasswordAuth\u003CAppUser>();  \u002F\u002F routes under \u002Fauth, cookie session by default\n",[53,214,215,240,270,274,290,305],{"__ignoreMap":51},[56,216,217,219,221,223,225,228,230,232,235,238],{"class":58,"line":59},[56,218,63],{"class":62},[56,220,28],{"class":66},[56,222,69],{"class":62},[56,224,28],{"class":66},[56,226,227],{"class":74},"AddPasswordAuth",[56,229,78],{"class":66},[56,231,82],{"class":81},[56,233,234],{"class":66},",",[56,236,237],{"class":81}," AppDbContext",[56,239,102],{"class":66},[56,241,242,244,246,248,250,253,255,258,260,262,265,268],{"class":58,"line":88},[56,243,63],{"class":62},[56,245,28],{"class":66},[56,247,69],{"class":62},[56,249,28],{"class":66},[56,251,252],{"class":74},"AddTransient",[56,254,78],{"class":66},[56,256,257],{"class":81},"IEmailSender",[56,259,78],{"class":66},[56,261,82],{"class":81},[56,263,264],{"class":66},">,",[56,266,267],{"class":81}," MyEmailSender",[56,269,102],{"class":66},[56,271,272],{"class":58,"line":105},[56,273,51],{"emptyLinePlaceholder":108},[56,275,276,278,280,282,284,286,288],{"class":58,"line":111},[56,277,115],{"class":114},[56,279,119],{"class":118},[56,281,123],{"class":122},[56,283,126],{"class":62},[56,285,28],{"class":66},[56,287,131],{"class":74},[56,289,134],{"class":66},[56,291,292,294,296,299,301],{"class":58,"line":137},[56,293,140],{"class":62},[56,295,28],{"class":66},[56,297,298],{"class":74},"UseAuthEndpoints",[56,300,134],{"class":66},[56,302,304],{"class":303},"ss7Ak","          \u002F\u002F authentication, authorization, rate limiting, antiforgery",[56,306,308,310,312,315,317,319,321],{"class":58,"line":307},6,[56,309,140],{"class":62},[56,311,28],{"class":66},[56,313,314],{"class":74},"MapPasswordAuth",[56,316,78],{"class":66},[56,318,82],{"class":81},[56,320,102],{"class":66},[56,322,323],{"class":303},"  \u002F\u002F routes under \u002Fauth, cookie session by default",[33,325,327],{"id":326},"the-mapidentityapi-gaps","The MapIdentityApi gaps",[329,330,332],"h3",{"id":331},"_1-no-logout-route","1. No logout route",[18,334,335],{},"MapIdentityApi doesn't map logout. Microsoft's docs tell you to write one:",[46,337,339],{"className":48,"code":338,"language":50,"meta":51,"style":51},"app.MapPost(\"\u002Flogout\", async (SignInManager\u003CIdentityUser> signInManager, [FromBody] object empty) =>\n{\n    if (empty != null)\n    {\n        await signInManager.SignOutAsync();\n        return Results.Ok();\n    }\n    return Results.Unauthorized();\n}).RequireAuthorization();\n",[53,340,341,405,410,430,435,449,464,470,485],{"__ignoreMap":51},[56,342,343,345,347,350,352,354,357,359,361,365,368,371,373,376,379,382,384,387,390,393,396,399,402],{"class":58,"line":59},[56,344,140],{"class":62},[56,346,28],{"class":66},[56,348,349],{"class":74},"MapPost",[56,351,148],{"class":66},[56,353,152],{"class":151},[56,355,356],{"class":155},"\u002Flogout",[56,358,152],{"class":151},[56,360,234],{"class":66},[56,362,364],{"class":363},"sTNss"," async",[56,366,367],{"class":66}," (",[56,369,370],{"class":81},"SignInManager",[56,372,78],{"class":66},[56,374,375],{"class":81},"IdentityUser",[56,377,378],{"class":66},">",[56,380,381],{"class":118}," signInManager",[56,383,234],{"class":66},[56,385,386],{"class":62}," [",[56,388,389],{"class":118},"FromBody",[56,391,392],{"class":62},"] ",[56,394,395],{"class":122},"object",[56,397,398],{"class":118}," empty",[56,400,401],{"class":66},")",[56,403,404],{"class":122}," =>",[56,406,407],{"class":58,"line":88},[56,408,409],{"class":66},"{",[56,411,412,416,418,421,424,428],{"class":58,"line":105},[56,413,415],{"class":414},"sRxSC","    if",[56,417,367],{"class":66},[56,419,420],{"class":62},"empty ",[56,422,423],{"class":122},"!=",[56,425,427],{"class":426},"sMTiH"," null",[56,429,401],{"class":66},[56,431,432],{"class":58,"line":111},[56,433,434],{"class":66},"    {",[56,436,437,440,442,444,447],{"class":58,"line":137},[56,438,439],{"class":122},"        await",[56,441,381],{"class":62},[56,443,28],{"class":66},[56,445,446],{"class":74},"SignOutAsync",[56,448,134],{"class":66},[56,450,451,454,457,459,462],{"class":58,"line":307},[56,452,453],{"class":414},"        return",[56,455,456],{"class":62}," Results",[56,458,28],{"class":66},[56,460,461],{"class":74},"Ok",[56,463,134],{"class":66},[56,465,467],{"class":58,"line":466},7,[56,468,469],{"class":66},"    }",[56,471,473,476,478,480,483],{"class":58,"line":472},8,[56,474,475],{"class":414},"    return",[56,477,456],{"class":62},[56,479,28],{"class":66},[56,481,482],{"class":74},"Unauthorized",[56,484,134],{"class":66},[56,486,488,491,494],{"class":58,"line":487},9,[56,489,490],{"class":66},"}).",[56,492,493],{"class":74},"RequireAuthorization",[56,495,134],{"class":66},[18,497,498,501],{},[53,499,500],{},"SignOutAsync()"," clears the cookie. It does nothing for bearer tokens. The bearer handler treats sign-out as a no-op, so the access and refresh tokens work until they expire.",[18,503,504,508,509,512,513,516],{},[505,506,507],"strong",{},"AuthEndpoints:"," every sign-in stack maps ",[53,510,511],{},"POST \u002Fauth\u002Flogout",". The cookie stack clears the Identity cookies and requires a CSRF token from ",[53,514,515],{},"GET \u002Fauth\u002FcsrfToken",". The JWT stack revokes the refresh token family and deletes the refresh cookie. The access JWT still works until it expires, 15 minutes by default. No logout can kill a JWT that's already out there.",[329,518,520],{"id":519},"_2-you-cant-drop-routes-like-register","2. You can't drop routes like \u002Fregister",[18,522,523,524,526,527,529,530,532],{},"You map all ten routes or none. ",[53,525,145],{}," adds a prefix and conventions. ",[53,528,145],{}," has no option to leave out ",[53,531,175],{}," for an invite-only app. In my opinion, once you need to customize MapIdentityApi past that, you end up copying its source and keeping it in sync yourself.",[18,534,535,537],{},[505,536,507],{}," if the facade maps too much, compose modules instead. For example, a JWT host without cookie login:",[46,539,541],{"className":48,"code":540,"language":50,"meta":51,"style":51},"var auth = app.MapGroup(\"\u002Fauth\");\nauth.MapIdentityManagementApi\u003CAppUser>();\nauth.MapJwtAuthEndpoints\u003CAppUser>();\n",[53,542,543,570,586],{"__ignoreMap":51},[56,544,545,547,550,552,554,556,558,560,562,565,567],{"class":58,"line":59},[56,546,115],{"class":114},[56,548,549],{"class":118}," auth",[56,551,123],{"class":122},[56,553,119],{"class":62},[56,555,28],{"class":66},[56,557,145],{"class":74},[56,559,148],{"class":66},[56,561,152],{"class":151},[56,563,564],{"class":155},"\u002Fauth",[56,566,152],{"class":151},[56,568,569],{"class":66},");",[56,571,572,575,577,580,582,584],{"class":58,"line":88},[56,573,574],{"class":62},"auth",[56,576,28],{"class":66},[56,578,579],{"class":74},"MapIdentityManagementApi",[56,581,78],{"class":66},[56,583,82],{"class":81},[56,585,102],{"class":66},[56,587,588,590,592,595,597,599],{"class":58,"line":105},[56,589,574],{"class":62},[56,591,28],{"class":66},[56,593,594],{"class":74},"MapJwtAuthEndpoints",[56,596,78],{"class":66},[56,598,82],{"class":81},[56,600,102],{"class":66},[18,602,603,606,607,610,611,613,614,617,618,620,621,623,624,626],{},[53,604,605],{},"MapPasswordEndpoints"," with ",[53,608,609],{},"PasswordLoginMode.None"," skips ",[53,612,179],{},". ",[53,615,616],{},"MapCookieSessionEndpoints"," maps only logout and the CSRF token. AuthEndpoints doesn't rename segments like ",[53,619,179],{}," either, and ",[53,622,314],{}," always includes ",[53,625,175],{},". To leave out sign-up, you compose the modules yourself.",[329,628,630],{"id":629},"_3-the-bearer-tokens-arent-jwts","3. The bearer tokens aren't JWTs",[18,632,633,634,636,637,176,640,176,643,202,646,649,650,653],{},"Call ",[53,635,179],{}," without query flags and you get ",[53,638,639],{},"accessToken",[53,641,642],{},"refreshToken",[53,644,645],{},"expiresIn",[53,647,648],{},"tokenType",". Microsoft's docs say the tokens \"aren't standard JSON Web Tokens\". They are data protection tickets. A second service can only read them with the Identity bearer handler and the same data protection keys. ",[53,651,652],{},"AddJwtBearer"," won't validate them.",[18,655,656,658],{},[505,657,507],{}," set the session to JWT.",[46,660,662],{"className":48,"code":661,"language":50,"meta":51,"style":51},"builder.Services.AddPasswordAuth\u003CAppUser, AppDbContext>(o =>\n{\n    o.Session = AuthEndpointsSession.Jwt;\n    o.Jwt = jwt =>\n    {\n        jwt.Issuer = \"https:\u002F\u002Fexample.com\";\n        jwt.Audience = \"https:\u002F\u002Fexample.com\";\n        jwt.SigningOptions.SymmetricKey = builder.Configuration[\"Jwt:SymmetricKey\"];\n    };\n});\n",[53,663,664,692,696,720,736,740,762,781,817,822],{"__ignoreMap":51},[56,665,666,668,670,672,674,676,678,680,682,684,687,690],{"class":58,"line":59},[56,667,63],{"class":62},[56,669,28],{"class":66},[56,671,69],{"class":62},[56,673,28],{"class":66},[56,675,227],{"class":74},[56,677,78],{"class":66},[56,679,82],{"class":81},[56,681,234],{"class":66},[56,683,237],{"class":81},[56,685,686],{"class":66},">(",[56,688,689],{"class":118},"o",[56,691,404],{"class":122},[56,693,694],{"class":58,"line":88},[56,695,409],{"class":66},[56,697,698,701,703,706,709,712,714,717],{"class":58,"line":105},[56,699,700],{"class":62},"    o",[56,702,28],{"class":66},[56,704,705],{"class":62},"Session ",[56,707,708],{"class":122},"=",[56,710,711],{"class":62}," AuthEndpointsSession",[56,713,28],{"class":66},[56,715,716],{"class":62},"Jwt",[56,718,719],{"class":66},";",[56,721,722,724,726,729,731,734],{"class":58,"line":111},[56,723,700],{"class":62},[56,725,28],{"class":66},[56,727,728],{"class":62},"Jwt ",[56,730,708],{"class":122},[56,732,733],{"class":118}," jwt",[56,735,404],{"class":122},[56,737,738],{"class":58,"line":137},[56,739,434],{"class":66},[56,741,742,745,747,750,752,755,758,760],{"class":58,"line":307},[56,743,744],{"class":62},"        jwt",[56,746,28],{"class":66},[56,748,749],{"class":62},"Issuer ",[56,751,708],{"class":122},[56,753,754],{"class":151}," \"",[56,756,757],{"class":155},"https:\u002F\u002Fexample.com",[56,759,152],{"class":151},[56,761,719],{"class":66},[56,763,764,766,768,771,773,775,777,779],{"class":58,"line":466},[56,765,744],{"class":62},[56,767,28],{"class":66},[56,769,770],{"class":62},"Audience ",[56,772,708],{"class":122},[56,774,754],{"class":151},[56,776,757],{"class":155},[56,778,152],{"class":151},[56,780,719],{"class":66},[56,782,783,785,787,790,792,795,797,799,801,804,807,809,812,814],{"class":58,"line":472},[56,784,744],{"class":62},[56,786,28],{"class":66},[56,788,789],{"class":62},"SigningOptions",[56,791,28],{"class":66},[56,793,794],{"class":62},"SymmetricKey ",[56,796,708],{"class":122},[56,798,126],{"class":62},[56,800,28],{"class":66},[56,802,803],{"class":62},"Configuration",[56,805,806],{"class":66},"[",[56,808,152],{"class":151},[56,810,811],{"class":155},"Jwt:SymmetricKey",[56,813,152],{"class":151},[56,815,816],{"class":66},"];",[56,818,819],{"class":58,"line":487},[56,820,821],{"class":66},"    };",[56,823,825],{"class":58,"line":824},10,[56,826,827],{"class":66},"});",[18,829,830,831,834,835,838,839,176,842,176,845,176,848,202,851,854],{},"You also call ",[53,832,833],{},"modelBuilder.UseRefreshToken()"," in your ",[53,836,837],{},"DbContext"," and add a migration. Then you get ",[53,840,841],{},"\u002Fauth\u002Fcreate",[53,843,844],{},"\u002Fauth\u002Frefresh",[53,846,847],{},"\u002Fauth\u002Fverify",[53,849,850],{},"\u002Fauth\u002Flogout",[53,852,853],{},"\u002Fauth\u002FcsrfToken",". Any service with the key can validate the access token.",[329,856,858],{"id":857},"_4-cookie-or-bearer-is-a-query-flag","4. Cookie or bearer is a query flag",[18,860,861,864,865,868],{},[53,862,863],{},"POST \u002Flogin?useCookies=true"," or ",[53,866,867],{},"?useSessionCookies=true"," returns the application cookie instead of tokens. The client picks the session type on each request. If you use the cookie path, you still add antiforgery, CORS and rate limits yourself, and there is no CSRF token endpoint.",[18,870,871,873,874,876],{},[505,872,507],{}," you pick the session in code. Cookie is the default, and ",[53,875,298],{}," turns on antiforgery and rate limits. Login, register and password reset don't need a CSRF token. Logout, account changes and passkey calls do.",[329,878,880],{"id":879},"_5-refresh-is-basic","5. Refresh is basic",[18,882,883,886],{},[53,884,885],{},"POST \u002Frefresh"," takes the refresh token and returns a new pair. MapIdentityApi has no logout for bearer tokens, so you can't end the old refresh token early.",[18,888,889,891,892,897],{},[505,890,507],{}," on the JWT stack the refresh token sits in a cookie and the server stores a hash of it. Each refresh rotates it. Per the ",[22,893,896],{"href":894,"rel":895},"https:\u002F\u002Fauthendpoints.harten.id\u002Fconcepts\u002Fcompare\u002F",[26],"AuthEndpoints compare page",", if someone reuses an old token, the whole family gets revoked.",[329,899,901],{"id":900},"_6-2fa-works-step-up-doesnt-exist","6. 2FA works, step-up doesn't exist",[18,903,904,907,908,910],{},[53,905,906],{},"POST \u002Fmanage\u002F2fa"," sets up authenticator and recovery codes, and ",[53,909,179],{}," takes the 2FA code. That part is fine. MapIdentityApi has no way to ask a signed-in user to prove it's them again before a sensitive change.",[18,912,913,915,916,202,919,28],{},[505,914,507],{}," same 2FA flow, plus step-up ReAuth through ",[53,917,918],{},"\u002Fmanage\u002FauthMethods",[53,920,921],{},"POST \u002FconfirmIdentity",[329,923,925],{"id":924},"_7-passkeys-are-only-coming-via-pr-68198","7. Passkeys are only coming via PR #68198",[18,927,928,929,934,935,938,939,942],{},"MapIdentityApi has no passkey routes in .NET 10. The Blazor template has passkey pages, but those belong to the template. ",[22,930,933],{"href":931,"rel":932},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Faspnetcore\u002Fpull\u002F68198",[26],"dotnet\u002Faspnetcore#68198",", \"Add passkey endpoints to MapIdentityApi\", was merged into ",[53,936,937],{},"main"," on 22 Sep 2026 with the ",[53,940,941],{},"12.0-preview1"," milestone. Passkey endpoints are coming to MapIdentityApi in a future release.",[18,944,945,947,948,951,952,176,955,958],{},[505,946,507],{}," set ",[53,949,950],{},"o.Passkeys.ServerDomain"," and you get ",[53,953,954],{},"\u002Fauth\u002Fpasskeys\u002Fregister",[53,956,957],{},"\u002Fauth\u002Fpasskeys\u002Flogin",", plus list, add, rename and delete. I wouldn't switch libraries for passkeys alone, since the framework is catching up.",[33,960,962],{"id":961},"the-gaps-at-a-glance","The gaps at a glance",[964,965,966,982],"table",{},[967,968,969],"thead",{},[970,971,972,976,979],"tr",{},[973,974,975],"th",{},"Gap",[973,977,978],{},"MapIdentityApi (.NET 10)",[973,980,981],{},"AuthEndpoints 4",[983,984,985,999,1010,1021,1032,1042],"tbody",{},[970,986,987,991,994],{},[988,989,990],"td",{},"Logout",[988,992,993],{},"Write your own",[988,995,996,998],{},[53,997,511],{}," per stack",[970,1000,1001,1004,1007],{},[988,1002,1003],{},"Drop routes",[988,1005,1006],{},"No",[988,1008,1009],{},"Compose modules",[970,1011,1012,1015,1018],{},[988,1013,1014],{},"JWT access token",[988,1016,1017],{},"No. Opaque tickets",[988,1019,1020],{},"Opt-in JWT session",[970,1022,1023,1026,1029],{},[988,1024,1025],{},"Session choice",[988,1027,1028],{},"Query flag per request",[988,1030,1031],{},"Set in code, cookie default",[970,1033,1034,1037,1039],{},[988,1035,1036],{},"Step-up",[988,1038,1006],{},[988,1040,1041],{},"ReAuth",[970,1043,1044,1047,1050],{},[988,1045,1046],{},"Passkeys",[988,1048,1049],{},"PR #68198, future release",[988,1051,1052],{},"Opt-in",[18,1054,1055,1056,28],{},"This only covers the gaps. Routes, request bodies and CSRF rules are in the ",[22,1057,1060],{"href":1058,"rel":1059},"https:\u002F\u002Fauthendpoints.harten.id\u002Fconcepts\u002Fcompare\u002F#vs-identity-api-endpoints-mapidentityapi",[26],"full AuthEndpoints vs MapIdentityApi comparison",[33,1062,1064],{"id":1063},"when-mapidentityapi-is-enough","When MapIdentityApi is enough",[18,1066,1067],{},"Use MapIdentityApi if the ten routes plus a logout handler you write cover your app. It fits internal tools, prototypes, and a single client that calls a single API with opaque tokens. It ships with the framework, Microsoft maintains it, and you skip a dependency.",[18,1069,1070],{},"I'd move off it when you need a logout that does something, JWTs another service can validate, fewer routes, or sign-in methods past a password, like email codes or GitHub and Google.",[33,1072,1074],{"id":1073},"why-a-library-when-agents-can-write-the-code","Why a library, when agents can write the code",[18,1076,1077],{},"I write a lot of code with AI agents now. They can generate a login endpoint in seconds, so writing code is cheap. Keeping that code correct and the same across every project is not. Each generated copy is one more place for a bug to hide, and one more version to patch.",[18,1079,1080],{},"Sharing code is still the fastest, easiest and cheapest way I know to cut bugs and keep things consistent. For auth, AuthEndpoints is that shared code for me. I fix a bug once, and every client project gets the fix when it updates the package.",[18,1082,1083],{},"AuthEndpoints also ships a skill for coding agents, so your agent sets it up the way the docs describe instead of guessing. Add it with one command:",[46,1085,1089],{"className":1086,"code":1087,"language":1088,"meta":51,"style":51},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark monokai","npx skills add madeyoga\u002FAuthEndpoints\n","bash",[53,1090,1091],{"__ignoreMap":51},[56,1092,1093,1097,1100,1103],{"class":58,"line":59},[56,1094,1096],{"class":1095},"sR7ES","npx",[56,1098,1099],{"class":155}," skills",[56,1101,1102],{"class":155}," add",[56,1104,1105],{"class":155}," madeyoga\u002FAuthEndpoints",[18,1107,1108,1109,1114],{},"Then ask your agent to add auth to your API. The ",[22,1110,1113],{"href":1111,"rel":1112},"https:\u002F\u002Fauthendpoints.harten.id\u002Fgetting-started\u002Fai-agents\u002F",[26],"AI agents guide"," has the details.",[33,1116,1118],{"id":1117},"faq","FAQ",[18,1120,1121,1124],{},[505,1122,1123],{},"What is a good MapIdentityApi alternative?","\nIf you want to stay on ASP.NET Core Identity and your own database, AuthEndpoints is one option. If you need a full OAuth or OpenID Connect server, OpenIddict is a better fit.",[18,1126,1127,1130,1131,606,1134,1137],{},[505,1128,1129],{},"Does MapIdentityApi have a logout endpoint?","\nNo. Microsoft's docs show how to write your own ",[53,1132,1133],{},"POST \u002Flogout",[53,1135,1136],{},"SignInManager.SignOutAsync()",". It doesn't revoke bearer tokens.",[18,1139,1140,1143,1144,28],{},[505,1141,1142],{},"Are MapIdentityApi tokens JWTs?","\nNo. They're data protection tickets. You validate them with the Identity bearer handler and the same keys, not ",[53,1145,652],{},[18,1147,1148,1151,1152,1154],{},[505,1149,1150],{},"Can I customize MapIdentityApi routes?","\nYou can add a prefix and conventions with ",[53,1153,145],{},". You can't remove or rename a single route.",[18,1156,1157,1160,1161,1163],{},[505,1158,1159],{},"Does MapIdentityApi support passkeys?","\nNot in .NET 10. PR #68198 adds passkey endpoints. It was merged into ",[53,1162,937],{}," in September 2026 with the 12.0-preview1 milestone.",[18,1165,1166,1169,1170,1172],{},[505,1167,1168],{},"Can I switch from MapIdentityApi to AuthEndpoints without changing my users table?","\nYes. Both use the same Identity store and request bodies. Your client paths move under ",[53,1171,564],{},", and the default login sets a cookie instead of returning tokens.",[18,1174,1175,1176,1181],{},"To try it, the ",[22,1177,1180],{"href":1178,"rel":1179},"https:\u002F\u002Fauthendpoints.harten.id\u002Fgetting-started\u002Fquick-start\u002F",[26],"quick start"," builds a small API with register, email confirmation, cookie sign-in and logout using SQLite and curl.",[1183,1184,1185],"style",{},"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 .swvn1, html code.shiki .swvn1{--shiki-light:#39ADB5;--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 .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 .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}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 .ss7Ak, html code.shiki .ss7Ak{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit;--shiki-sepia:#88846F;--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}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 .sMTiH, html code.shiki .sMTiH{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sR7ES, html code.shiki .sR7ES{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}",{"title":51,"searchDepth":88,"depth":105,"links":1187},[1188,1189,1198,1199,1200,1201],{"id":35,"depth":88,"text":36},{"id":326,"depth":88,"text":327,"children":1190},[1191,1192,1193,1194,1195,1196,1197],{"id":331,"depth":105,"text":332},{"id":519,"depth":105,"text":520},{"id":629,"depth":105,"text":630},{"id":857,"depth":105,"text":858},{"id":879,"depth":105,"text":880},{"id":900,"depth":105,"text":901},{"id":924,"depth":105,"text":925},{"id":961,"depth":88,"text":962},{"id":1063,"depth":88,"text":1064},{"id":1073,"depth":88,"text":1074},{"id":1117,"depth":88,"text":1118},"2026-10-23","MapIdentityApi has no logout route, no way to drop \u002Fregister, and tokens that aren't JWTs. Here are the gaps I hit and how AuthEndpoints 4 covers them.","md",null,{"sitemap":1207,"schemaOrg":1208},{"lastmod":1202},[1209],{"type":1210,"headline":5,"author":1211,"datePublished":1202},"BlogPosting",{"type":1212,"name":1213},"Person","Made Yoga","\u002Fblog\u002Fmapidentityapi-alternative",{"keywords":1216,"title":5,"description":1203},"MapIdentityApi, MapIdentityApi alternative, ASP.NET Core Identity, AuthEndpoints, dotnet, aspnetcore, authentication, csharp","blog\u002Fmapidentityapi-alternative","ED6n_ZeDDZYac3bxavvY3be826cgj-JKJi6BHH_7A28",[1220,1205],{"title":1221,"path":1222,"stem":1223,"description":1224,"children":-1},"Hermes Agent Bot Mode: how I split work across three specialist bots","\u002Fblog\u002Fhermes-bot-mode","blog\u002Fhermes-bot-mode","How I run Hermes Agent Bot Mode with three specialist bots (Blogi, Nuxti, Aspi): what a Bot is, how they message each other, and what still feels unfinished.",{"id":1226,"title":1227,"body":1205,"description":1205,"experiences":1228,"extension":1252,"meta":1253,"navigation":108,"path":1260,"personal":1261,"seo":1279,"stem":1280,"values":1281,"__hash__":1305},"about\u002Fabout.yml","About Me",[1229,1236,1241,1246],{"timeframe":1230,"title":1231,"company":1232,"domainName":1233,"url":1234,"jobtype":1235},"2022 - 2026","Software Engineer","Superjaya","superjaya.id","https:\u002F\u002Fsuperjaya.id","Freelance",{"timeframe":1237,"title":1238,"company":1239,"domainName":1240,"url":51,"jobtype":1235},"2025 - Present","Backend Developer","Prima (Medicine Store System)","Internal",{"timeframe":1242,"title":1231,"company":1243,"domainName":1244,"url":1245,"jobtype":1235},"2021 - 2022","PT Media Bersama Sukses","mbscctv.com","https:\u002F\u002Fmbscctv.com",{"timeframe":1247,"title":1248,"company":1249,"domainName":1250,"url":1251,"jobtype":1235},"2024 - 2024","Frontend Software Developer","PT. Nasional Bhirawa Tama","nasionalrubber.co.id","https:\u002F\u002Fnasionalrubber.co.id\u002F","yml",{"sitemap":1254,"schemaOrg":1256},{"lastmod":1255},"2026-08-01",[1257],{"type":1258,"name":1259},"AboutPage","About Made Yoga Mahardika","\u002Fabout",{"name":1262,"title":1263,"bio":1264,"image":11,"location":1267,"links":1269},"Made Yoga Mahardika",{"en":1231,"id":1231},{"en":1265,"id":1266},"I build software for real businesses, from back-office systems to customer-facing platforms. I also maintain AuthEndpoints, an open-source auth library for ASP.NET Core, and tinker with AI agents and LLM tooling out in the open.","Saya membangun software untuk bisnis nyata, dari sistem back-office sampai platform pelanggan. Saya juga mengelola AuthEndpoints, pustaka auth open source untuk ASP.NET Core, dan mengutak-atik AI agents serta tooling LLM secara terbuka.",{"en":1268,"id":1268},"Samarinda, Indonesia",[1270,1273,1276],{"icon":1271,"url":1272},"i-simple-icons-github","https:\u002F\u002Fgithub.com\u002Fmadeyoga",{"icon":1274,"url":1275},"i-simple-icons-x","https:\u002F\u002Fx.com\u002Fmadey_bog",{"icon":1277,"url":1278},"i-simple-icons-youtube","https:\u002F\u002Fwww.youtube.com\u002F@madey02",{"title":1227},"about",[1282,1290,1298],{"icon":1283,"title":1284,"description":1287},"i-lucide-shield-check",{"en":1285,"id":1286},"Integrity","Integritas",{"en":1288,"id":1289},"I make sure clients profit from the apps I build. Their problem has to get solved, and the software has to earn its keep.","Saya memastikan klien untung dari aplikasi yang saya bangun. Masalah mereka harus selesai, dan software-nya harus layak untuk bisnis mereka.",{"icon":1291,"title":1292,"description":1295},"i-lucide-check",{"en":1293,"id":1294},"Practical results","Hasil nyata",{"en":1296,"id":1297},"I solve business problems first. The code is just how the fix ships.","Saya menyelesaikan masalah bisnis dulu. Coding cuma cara solusinya dikirim.",{"icon":1291,"title":1299,"description":1302},{"en":1300,"id":1301},"Adaptability","Adaptif",{"en":1303,"id":1304},"Requirements change. I adjust, and I'll pick up whatever tool or workflow fits the job.","Kebutuhan bisa berubah. Saya menyesuaikan diri dan memakai alat atau metode apa pun yang cocok untuk pekerjaannya.","kCVNH8YbeigB_asfS68mgsV8YDmO0v5Irj-c03bdn4g",1792758484281]