Skip to content

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.Identity schema migrations.
  • Implement IEmailSender<AppUser> (SendGrid, Mailgun, SMTP).
  • Decide token storage — default uses memory; for clusters, configure persistence.
  • Add 2FA UX if needed (/manage/2fa exposes 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

// Frontend tries to use it as an OIDC provider — fails: no /.well-known/openid-configuration

✅ 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>/forgotPassword works but no mail is sent.
  • Trying to federate with Google via /login — that requires manual external-login plumbing.
  • Pinning to ApplicationScheme for API auth — that's the cookie scheme; APIs need BearerScheme.

Further reading