Introducing the Identity API endpoints: Exploring the .NET 8 preview – Part 8

ASP.NET Core Identity has been around for a long time, and it does one job very well: storing user accounts, handling passwords, and managing two factor authentication inside your own application. What it never did well was give you a clean way to expose that functionality as an API. If you were building a Blazor WebAssembly app, a mobile client, or any SPA that needed token based authentication, you were mostly on your own. .NET 8 preview 7 changes that with a new set of built in minimal API endpoints for Identity. This article walks through what these endpoints are, how to wire them into a project, and more importantly, when you should and should not reach for them in a real application.

Why ASP.NET Core Identity needed API endpoints

ASP.NET Core Identity is built around three abstractions. The data model defines the core types such as IdentityUser and IdentityRole. The stores are the persistence layer, with interfaces like IUserStore and IRoleStore that talk to your database. On top of the stores sit the managers: UserManager for creating and updating users, RoleManager for role based operations, and SignInManager for handling the actual sign in and sign out flow, including lockouts and two factor checks.

These managers give you relatively high level APIs, but you still have to call them from somewhere. Microsoft has shipped a default UI package for years, Microsoft.AspNetCore.Identity.UI, which drops in more than thirty Razor Pages covering login, registration, password reset, email confirmation, and 2FA management. It saves a huge amount of boilerplate, but it comes with real costs once you scaffold it into your project to customise the styling.

Core ASP.NET Core Identity abstractions: managers on top of stores, backed by EF Core and the database
Core ASP.NET Core Identity abstractions: managers on top of stores, backed by EF Core and the database

If you wanted to use Identity to store users inside your own app, you were stuck with one of three unappealing choices. You could use the default Razor Pages UI as is and live with limited theming, fine for an internal tool but not much else. You could scaffold the pages and hand edit dozens of Razor files, which then need to be kept in sync every time you upgrade. Or you could write the entire login and account management flow yourself, which is a lot of surface area to get right in the most security sensitive part of your app. None of these work well if you are building a SPA or a mobile client, since there was also no built in way to issue a bearer token instead of a cookie.

What the new Identity API endpoints actually do

In .NET 8, ASP.NET Core adds a set of minimal API endpoints that mirror what the default Razor Pages UI already does, but expose it as JSON over HTTP instead of server rendered pages. This solves two problems at once. You can build your own login and registration screens in your SPA using these APIs, keeping the visual style consistent with the rest of your app. And you can now get an access token back from a login call instead of only a cookie, which is what mobile and SPA clients actually want.

The default Razor Pages Identity login screen. It signs the user in by setting a cookie, which is awkward for mobile and SPA clients
The default Razor Pages Identity login screen. It signs the user in by setting a cookie, which is awkward for mobile and SPA clients

With the default Razor Pages UI, cookies were the only outcome of signing in, and cookies are painful to manage from a native mobile client. The new endpoints make retrieving a token as simple as a POST request. Here is what calling the login endpoint looks like once it is wired up:

POST http://localhost:5117/account/login
Content-Type: application/json
 
{
  "username": "andrew@example.com",
  "password": "SuperSecret1!"
}

A successful call returns a JSON body with a bearer token, an expiry, and a refresh token, similar to this (tokens truncated here for readability):

{
  "token_type": "Bearer",
  "access_token": "CfDJ8CuDyfVIT-VKm_2z2YS9T0jen4IyKKwsovVDRrrFyC...",
  "expires_in": 3600,
  "refresh_token": "CfDJ8CuDyfVIT-VKm_2z2YS9T0gvL1EYfbVBnppccNrI6..."
}

The access token is short lived by design, and the client is expected to call the refresh endpoint with the refresh token once it expires. Nothing exotic here if you have worked with any token based auth flow before, but it is a genuine gap that ASP.NET Core Identity has filled for the first time.

Design considerations before you adopt this

This is the part that matters most if you are deciding whether to use this feature on a real project, so read it carefully before you start wiring it into anything customer facing. Behind the scenes, these endpoints use the exact same APIs the default Identity UI already uses. If you have an existing Identity setup, plugging these endpoints in is usually straightforward.

The important detail is that the access token returned here is not a JWT. It carries roughly the same information as the Identity cookie used by the default UI, just wrapped up as an opaque token string instead of a cookie. That makes the endpoint easy to implement on Microsoft’s side, but it also tells you exactly what this feature is for and what it is not for.

These endpoints are not a replacement for a proper token server such as Duende IdentityServer or OpenIddict. The tokens they issue are meant for that specific app and that app alone, and they are meant for interactive user sessions, not machine to machine communication. If you are building a simple backend plus SPA combination, this might be tempting as a quick win. If you are building anything more substantial, with multiple client apps, delegated access to third party APIs, or any need for standard OAuth 2.0 and OpenID Connect flows, you are better off reaching for a real OIDC server or an identity platform like Microsoft Entra ID from day one.

There is also a security trade off worth calling out. Using the Identity endpoints with bearer tokens carries more exposure to token theft and impersonation risk than the cookie plus OIDC pattern that most production apps have settled on, mainly because there is no token binding and revocation is limited compared to a dedicated identity provider. Treat this as a fast path for prototypes, internal tools, and hobby projects, not as the authentication layer for anything handling real user data at scale.

Setting up the Identity API endpoints in a new project

The Identity endpoints are not part of any dotnet new template yet. The webapi template does have an –auth flag, but at the preview 7 stage it only supports Azure AD, Azure AD B2C, or Windows authentication, not the new Identity endpoints. So you wire it up manually, starting from a blank Web API project.

dotnet new webapi

From here there are five things to add: the EF Core packages, a DbContext, the Identity EF Core models with a migration, the Identity services and endpoints themselves, and finally the authorization middleware. None of these steps are complicated on their own, but skipping one of them produces a confusing runtime error, so it helps to go through them in order.

Adding EF Core

SQLite works well for a demo like this since it needs no separate server to install. You would not want SQLite backing a production web app under real concurrent load, but for trying the feature out it keeps things simple. Add the three packages below with the prerelease flag so you pull in the .NET 8 preview builds.

# The main package for SQLite EF Core support
dotnet add package Microsoft.EntityFrameworkCore.SQLite --prerelease
 
# Contains shared build-time components for EF Core
dotnet add package Microsoft.EntityFrameworkCore.Design --prerelease
 
# The ASP.NET Core Identity integration for EF Core
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --prerelease

You will also need the dotnet-ef tool installed and updated to a prerelease version, otherwise migration commands later in this walkthrough will fail with a version mismatch.

dotnet tool update --global dotnet-ef --prerelease

With the packages and tooling in place, define a minimal DbContext to start with. You will extend this shortly once Identity comes into the picture.

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
     : base(options)
    {
    }
}

Register it against SQLite in Program.cs, pointing at a connection string named DefaultConnection.

using Microsoft.EntityFrameworkCore;
 
var builder = WebApplication.CreateBuilder(args);
 
// Add EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

Then add the matching connection string to appsettings.json.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=my_test_app.db"
  }
}

Wiring up Identity and the Identity API endpoints

Before adding the Identity services, create a user type deriving from IdentityUser, and change the DbContext to derive from IdentityDbContext instead of the plain DbContext. You could use IdentityUser directly without a custom subclass, but almost every real app ends up needing to add a field or two to the user record later, so it is worth doing this from the start rather than refactoring midway through a project.

public class AppUser : IdentityUser
{
    // Add customisations here later
}
 
// Change from DbContext to IdentityDbContext<>
public class AppDbContext : IdentityDbContext<AppUser>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
}

With those two types in place, register the Identity services using the new AddIdentityApiEndpoints method and point it at the EF Core stores.

builder.Services
    .AddIdentityApiEndpoints<AppUser>()
    .AddEntityFrameworkStores<AppDbContext>();

This single call does three things behind the scenes. It configures both bearer and cookie authentication schemes, it registers the core Identity services such as UserManager, and it adds the extra services the API endpoints need, including SignInManager, the token providers, and a no-op IEmailSender implementation so registration does not fail just because you have not configured a real email sender yet.

Now generate a migration and update the database. If any of the earlier steps were missed, this is usually where the build fails first, so treat a clean migration as a sign the setup so far is correct.

dotnet ef migrations add InitialSchema
dotnet ef database update

Protecting an endpoint and adding authorization middleware

To confirm the whole flow actually works end to end, add an authorization requirement on the default weather forecast endpoint that ships with the webapi template.

app.MapGet("/weatherforecast", () => /* not shown for brevity */)
  .WithName("GetWeatherForecast")
  .RequireAuthorization() // Add this
  .WithOpenApi();

Running the app at this point throws an exception rather than a clean 401, because the authorization middleware has not been registered yet.

System.InvalidOperationException: Endpoint HTTP: GET /weatherforecast contains
authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() in the application
startup code.

This is a common trap when wiring up authorization from scratch on a minimal API project, since RequireAuthorization only attaches metadata, it does not add the middleware for you. Fix it by registering the authorization services and adding the middleware in the right place in the pipeline, between routing and endpoint execution.

var builder = WebApplication.CreateBuilder(args);
 
// Add the authorization services
builder.Services.AddAuthorization();
 
// Add EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
 
// add identity services
builder.Services
    .AddIdentityApiEndpoints<AppUser>()
    .AddEntityFrameworkStores<AppDbContext>();
 
var app = builder.Build();
 
app.UseHttpsRedirection();
app.UseAuthorization(); // Add Authorization middleware

Finally, map the Identity endpoints themselves. It is worth scoping them under a prefix such as /account rather than mounting them at the root, so /login and /register do not collide with anything else in your API surface.

app.MapGroup("/account").MapIdentityApi<AppUser>();

Trying out the endpoints

Run the app and open /swagger/index.html to see everything MapIdentityApi added. Alongside the weatherforecast endpoint from the template, you get register, login, refresh, confirmEmail, resendConfirmationEmail, resetPassword, and a handful of 2FA and account info endpoints.

Swagger UI showing the full set of endpoints added by MapIdentityApi, alongside the weatherforecast endpoint from the template
Swagger UI showing the full set of endpoints added by MapIdentityApi, alongside the weatherforecast endpoint from the template

Calling the protected weatherforecast endpoint without a token returns exactly what you would expect, a 401 with a WWW-Authenticate header pointing at Bearer authentication.

GET http://localhost:5117/weatherforecast
 
HTTP/1.1 401 Unauthorized
Content-Length: 0
WWW-Authenticate: Bearer

That confirms the endpoint is actually locked down before you go any further, which is worth checking explicitly rather than assuming RequireAuthorization is doing its job.

Registering a user and retrieving tokens

The register endpoint takes an email, username, and password, and returns a plain 200 on success.

### Register a new user
POST http://localhost:5117/account/register
Content-Type: application/json
 
{
  "username": "andrew@example.com",
  "password": "SuperSecret1!",
  "email": "andrew@example.com"
}

The password has to satisfy the default IdentityOptions rules for length and complexity, so a weak password gets rejected with a validation error rather than a generic failure. One practical suggestion worth passing on here: drop most of the default complexity rules such as forcing a mix of symbols and digits, raise the minimum length instead, and plug in a validator that checks against known breached password lists like the one haveibeenpwned.com exposes. Length beats complexity for real world password strength, and breached password checks catch far more actual account takeover attempts than symbol requirements ever will.

Once the user exists, call login to get a token pair back.

### Login and retrieve tokens
POST http://localhost:5117/account/login
Content-Type: application/json
 
{
  "username": "andrew@example.com",
  "password": "SuperSecret1!"
}
{
  "token_type": "Bearer",
  "access_token": "CfDJ8CuDyfVIT-VKm_2z2YS9T0jen4IyKKwsovVDRrrFyC...",
  "expires_in": 3600,
  "refresh_token": "CfDJ8CuDyfVIT-VKm_2z2YS9T0gvL1EYfbVBnppccNrI6..."
}

If you are testing with a JetBrains Rider HTTP client or a Visual Studio .http file, you can script the response to automatically stash the tokens into variables for the next request, instead of copy pasting them by hand each time.

> {%
    client.global.set("access_token", response.body.access_token);
    client.global.set("refresh_token", response.body.refresh_token);
%}
Scripting an HTTP client request to capture the access and refresh tokens automatically after login
Scripting an HTTP client request to capture the access and refresh tokens automatically after login

This small scripting trick saves a lot of manual copy pasting once you are testing more than a couple of endpoints in a session, and it is a pattern worth reusing for any API that returns tokens or IDs you need in a follow up call.

Calling a protected endpoint and refreshing the token

With the access token captured, pass it as a Bearer token on the protected endpoint.

### Call Forecast API with bearer token
GET http://localhost:5117/weatherforecast
Authorization: Bearer {{access_token}}

This time the call succeeds and returns the JSON payload the endpoint normally produces, confirming the token is being validated correctly against the Identity bearer scheme.

[
  { "date": "2023-09-03", "temperatureC": 20, "summary": "Chilly", "temperatureF": 67 },
  ...
]

When the access token eventually expires, call refresh with the stored refresh token to get a new pair. Note that the response also returns a new refresh token, so make sure your client code overwrites the old one each time rather than reusing a stale value.

### Fetch a new access token
POST http://localhost:5117/account/refresh
Content-Type: application/json
 
{
  "refreshToken": "{{refresh_token}}"
}

There are additional endpoints for managing 2FA enrollment and account info that follow the same pattern, so once you understand register, login, and refresh, the rest of the surface is not hard to pick up.

Should you use this in production

For an internal admin tool, a proof of concept, or a small side project where you control both the client and the API, the Identity API endpoints are a genuinely useful shortcut. You get registration, login, token refresh, and 2FA management without writing any of that plumbing yourself, and the setup takes an afternoon rather than a sprint.

For anything customer facing, or any system where you expect to add more client applications over time, plan for Duende IdentityServer, OpenIddict, or a managed platform like Microsoft Entra ID from the outset instead. Those give you standard OAuth 2.0 and OpenID Connect flows, proper JWT access tokens that other services can validate independently, consent screens, and a token revocation model that the Identity endpoints simply do not offer. Migrating off the Identity endpoints once a client app depends on their token format is more painful than choosing the right foundation up front.

It also helps to remember that the underlying Identity system, the UserManager, SignInManager, and EF Core stores, is not going anywhere and works exactly as before. What is new is only the API surface on top. You can still fall back to the default Razor Pages UI, or build your own UI directly against the managers, if the new endpoints do not fit your use case.

Wrapping up

The new Identity API endpoints fill a real gap that has existed in ASP.NET Core Identity for years: a supported way to get token based authentication out of Identity without hand rolling your own controller actions. Setup is straightforward once you know the five pieces involved, EF Core, the Identity models, AddIdentityApiEndpoints, authorization middleware, and MapIdentityApi. The main judgment call is deciding whether your app’s authentication needs are simple enough for this built in option, or whether you need the fuller feature set of a dedicated identity provider. Since this was still preview 7 at the time of writing, some details are likely to change before .NET 8 ships, so treat exact endpoint names and response shapes as subject to revision at general availability.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading