Identity Endpoints (MapIdentityApi)
Key Points
MapIdentityApi<TUser>()(.NET 8+) wires up minimal-API endpoints for ASP.NET Core Identity: register, login, refresh, confirm email, password reset.- Issues bearer tokens (opaque, server-side validated) by default, with refresh tokens for long-lived sessions. Optional cookie support.
- Right fit for: SPAs and mobile apps that talk to your own API, without standing up a full IdentityServer / OpenIddict / Duende deployment.
- Wrong fit for: third-party identity (let users sign in with Google/Microsoft), tenant-isolation across many apps, OAuth2 client-credentials. Use Entra ID / Auth0 / Duende for those.
- Not OIDC — these are proprietary endpoints with a custom token format. Don't conflate with OpenID Connect.
Concepts (deep dive)
Setup
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opts => opts.UseSqlServer(connStr));
builder.Services.AddIdentityCore<AppUser>()
.AddEntityFrameworkStores<AppDbContext>()
.AddApiEndpoints(); // ← critical: enables MapIdentityApi
builder.Services.AddAuthentication()
.AddBearerToken(IdentityConstants.BearerScheme);
builder.Services.AddAuthorization();
var app = builder.Build();
app.MapIdentityApi<AppUser>(); // /register, /login, /refresh, /confirmEmail, ...
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/me", (ClaimsPrincipal u) => $"Hi {u.Identity!.Name}").RequireAuthorization();
app.Run();
public class AppUser : IdentityUser { }
public class AppDbContext(DbContextOptions<AppDbContext> o) : IdentityDbContext<AppUser>(o) { }
After EF migrations, the database has AspNetUsers, AspNetRoles, etc.
Endpoints created
POST /register body: { email, password }
POST /login body: { email, password } → access + refresh
POST /refresh body: { refreshToken } → new access + refresh
POST /confirmEmail body: { userId, code }
POST /resendConfirmationEmail
POST /forgotPassword
POST /resetPassword
POST /manage/2fa
POST /manage/info
GET /manage/info
POST /logout
Token format
Default: opaque bearer tokens (random GUIDs server-stores), validated against the IdentityBearerOptions data store. Not JWT by default. Set BearerTokenOptions.BearerTokenExpiration and RefreshTokenExpiration to control lifetimes.
builder.Services.Configure<BearerTokenOptions>(IdentityConstants.BearerScheme, opts =>
{
opts.BearerTokenExpiration = TimeSpan.FromMinutes(30);
opts.RefreshTokenExpiration = TimeSpan.FromDays(14);
});
Bearer + cookies dual auth
builder.Services.AddAuthentication()
.AddBearerToken(IdentityConstants.BearerScheme)
.AddCookie(IdentityConstants.ApplicationScheme);
Now /login accepts a useCookies=true query: returns a cookie instead of bearer. Useful if part of your app is server-rendered and part is SPA.
Securing endpoints
app.MapGet("/orders", () => /* ... */).RequireAuthorization();
app.MapGet("/admin", () => /* ... */).RequireAuthorization(p => p.RequireRole("Admin"));
AppUser claims drive ClaimsPrincipal.
Email confirmation flow
POST /register → creates user, generates confirm code
(depending on options) sends email with link to /confirmEmail?userId=...&code=...
POST /confirmEmail → marks email confirmed; user can now /login
You implement IEmailSender<AppUser> to send the actual mail.
Password reset
POST /forgotPassword { email }
→ IEmailSender called with reset code
POST /resetPassword { email, resetCode, newPassword }
→ password updated
Reset codes are server-generated time-bound tokens.
MapIdentityApi vs full IdentityServer / Duende / OpenIddict
| Concern | MapIdentityApi | IdentityServer/Duende/OpenIddict |
|---|---|---|
| Single API, single audience | ✓ | ✓ |
| Multiple APIs, SSO across them | ✗ | ✓ |
| External logins (Google/MS) | partial (manual hookup) | first-class |
| OAuth2 client credentials | ✗ | ✓ |
OIDC discovery / .well-known | ✗ | ✓ |
| Custom consent UI | ✗ | ✓ |
| Federation, SAML | ✗ | possible |
If you need any of the right column, choose Duende (commercial), OpenIddict, Auth0, or Entra ID instead.
What's still your job
- Set up
Microsoft.AspNetCore.Identityschema migrations. - Implement
IEmailSender<AppUser>(SendGrid, Mailgun, SMTP). - Decide token storage — default uses memory; for clusters, configure persistence.
- Add 2FA UX if needed (
/manage/2faexposes the API; UI is yours).
Code: correct vs wrong
❌ Wrong: forgetting .AddApiEndpoints()
builder.Services.AddIdentityCore<AppUser>()
.AddEntityFrameworkStores<AppDbContext>();
// MapIdentityApi will throw at runtime: "Identity API endpoints require AddApiEndpoints"
✅ Correct
builder.Services.AddIdentityCore<AppUser>()
.AddEntityFrameworkStores<AppDbContext>()
.AddApiEndpoints();
❌ Wrong: relying on it for OIDC
✅ Correct: choose Duende/OpenIddict for OIDC
MapIdentityApi is for "this API authenticates its own users." Not federation.
❌ Wrong: long-lived bearer tokens
opts.BearerTokenExpiration = TimeSpan.FromDays(30); // can't be revoked except by deleting refresh token
✅ Correct: short-lived access + refresh
opts.BearerTokenExpiration = TimeSpan.FromMinutes(15);
opts.RefreshTokenExpiration = TimeSpan.FromDays(14);
Design patterns for this topic
Pattern 1 — Standard SPA auth
SPA → POST /login → stores access + refresh in IndexedDB
SPA → calls /api/* with Authorization: Bearer <access>
On 401: SPA → POST /refresh → new tokens; retry
On user-initiated logout: POST /logout
Pattern 2 — Custom user fields
public class AppUser : IdentityUser
{
public string? DisplayName { get; set; }
public DateTime? LastLogin { get; set; }
}
Migrations regenerate. Access via UserManager<AppUser> from DI.
Pattern 3 — Add external login on top
builder.Services.AddAuthentication()
.AddBearerToken(IdentityConstants.BearerScheme)
.AddGoogle(opts => { opts.ClientId = "..."; opts.ClientSecret = "..."; });
You then add a custom endpoint or the Identity UI to handle the Google flow and create the user.
Pattern 4 — Custom claims principal factory
public class CustomClaimsFactory : UserClaimsPrincipalFactory<AppUser>
{
public override async Task<ClaimsPrincipal> CreateAsync(AppUser user)
{
var p = await base.CreateAsync(user);
((ClaimsIdentity)p.Identity!).AddClaim(new Claim("display_name", user.DisplayName ?? ""));
return p;
}
}
builder.Services.AddScoped<IUserClaimsPrincipalFactory<AppUser>, CustomClaimsFactory>();
Pros & cons / trade-offs
| Aspect | MapIdentityApi | Full IdP (Entra/Auth0/Duende) |
|---|---|---|
| Setup time | Hours | Days |
| Cross-app SSO | No | Yes |
| OIDC discovery | No | Yes |
| External logins | Manual | First-class |
| Self-hosted | Yes | Yes (Duende/OpenIddict) or SaaS |
| Compliance reports (SOC, ISO) | DIY | Often included (SaaS) |
When to use / when to avoid
- Use for single-tenant API + SPA where you want to own the user database.
- Use for prototypes, MVPs, internal tools.
- Avoid for B2B SaaS with multi-tenant SSO (use Entra ID / Auth0).
- Avoid for replacing OIDC — these endpoints aren't OIDC.
- Avoid for multiple APIs needing shared sign-in — bearer tokens here are scoped to this app.
Interview Q&A
Q1. What does MapIdentityApi add to your app? A. Minimal-API endpoints for register/login/refresh/etc., backed by ASP.NET Core Identity stores. Bearer-token-based by default; cookies optional.
Q2. Are these JWT tokens? A. No — opaque bearer tokens (random server-stored values) by default. Validated by querying the data store. Choose JWT only via custom configuration; the default is intentionally opaque for revocation simplicity.
Q3. What's the relationship to OIDC? A. None — these endpoints don't expose /.well-known/openid-configuration, don't issue ID tokens, and aren't OAuth2 standards-compliant. They're a proprietary wrapper around Identity for first-party authentication.
Q4. When would you choose Duende/OpenIddict over MapIdentityApi? A. When you need OIDC, multi-app SSO, third-party logins as a first-class concern, or to act as an authorization server for external clients. MapIdentityApi is "auth for this one API."
Q5. What does .AddApiEndpoints() do? A. Registers the endpoint route mappings + handler services that MapIdentityApi consumes. Without it, the runtime throws on MapIdentityApi.
Q6. How do you customize the user model? A. Subclass IdentityUser with extra properties; use it as the generic argument for AddIdentityCore<TUser> and MapIdentityApi<TUser>. Run migrations.
Q7. Where are tokens stored? A. By default in memory plus the Identity stores. For clustered deployments configure persistence (IUserStore, refresh-token tables). Or switch to DataProtection-keyed schemes.
Q8. Can you use cookies and bearer tokens simultaneously? A. Yes — register both schemes; clients pass useCookies=true to /login for cookie-based auth; default returns bearer. Useful for hybrid server-rendered + SPA apps.
Gotchas / common mistakes
- Forgetting
.AddApiEndpoints()— runtime exception. - Treating opaque bearer tokens as JWT — they don't decode.
- Returning user emails in tokens — bearer is opaque; user info is via
/manage/info. - Long bearer-token lifetimes — hard to revoke. Use short access + refresh.
- Skipping
IEmailSender<AppUser>—/forgotPasswordworks but no mail is sent. - Trying to federate with Google via
/login— that requires manual external-login plumbing. - Pinning to
ApplicationSchemefor API auth — that's the cookie scheme; APIs needBearerScheme.