Changelog
Get started

Quick start: add cookie sign-in to an API

Build a minimal ASP.NET Core API where a user registers, confirms their email, signs in with a cookie, and signs out.

In this tutorial you build a minimal API that uses AuthEndpoints for cookie sign-in. By the end, a user can register, confirm their email, sign in, call a protected endpoint, and sign out. You use curl as the client, so you need no front end.

You need the .NET 10 SDK.

Create the project

Create an empty web project and add the packages:

dotnet new web -n AuthDemo
cd AuthDemo
dotnet add package AuthEndpoints
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

The tutorial uses SQLite so you can run it without a database server. Any EF Core provider works.

Wire the host

Replace Program.cs with this code:

using AuthEndpoints;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite("Data Source=app.db"));

builder.Services.AddAuthEndpoints<AppUser, AppDbContext>(o =>
{
    o.Passkeys.ServerDomain = "localhost";
});

builder.Services.AddTransient<IEmailSender<AppUser>, ConsoleEmailSender>();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreated();
}

app.UseAuthEndpoints();
app.MapAuthEndpoints<AppUser>();

app.MapGet("/me", (System.Security.Claims.ClaimsPrincipal user) => new { email = user.Identity!.Name })
    .RequireAuthorization();

app.Run();

public class AppUser : IdentityUser { }

public class AppDbContext(DbContextOptions<AppDbContext> options) : IdentityDbContext<AppUser>(options) { }

public class ConsoleEmailSender(ILogger<ConsoleEmailSender> logger) : IEmailSender<AppUser>
{
    public Task SendConfirmationLinkAsync(AppUser user, string email, string confirmationLink)
    {
        logger.LogInformation("Confirm {Email}: {Link}", email, confirmationLink);
        return Task.CompletedTask;
    }

    public Task SendPasswordResetLinkAsync(AppUser user, string email, string resetLink)
    {
        logger.LogInformation("Reset link for {Email}: {Link}", email, resetLink);
        return Task.CompletedTask;
    }

    public Task SendPasswordResetCodeAsync(AppUser user, string email, string resetCode)
    {
        logger.LogInformation("Reset code for {Email}: {Code}", email, resetCode);
        return Task.CompletedTask;
    }
}

Here is what the AuthEndpoints calls do:

  • AddAuthEndpoints<AppUser, AppDbContext> registers Identity with EF Core stores, the application cookie, passkeys, step-up, antiforgery, and the rate-limit policies.
  • UseAuthEndpoints adds authentication, authorization, rate limiting, and antiforgery middleware. In a real app, call it after your exception-handling middleware.
  • MapAuthEndpoints<AppUser> maps the routes under /identity and /account.

ConsoleEmailSender writes links to the log so you can follow them by hand. EnsureCreated builds the schema without migrations. Both are for local use only. See Production before you deploy.

Run the app

dotnet run

Note the URL in the Now listening on log line. The commands below use http://localhost:5265. Replace it with your URL. Open a second terminal for the curl commands.

Register a user

curl -i -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"Passw0rd!"}' \
  http://localhost:5265/identity/register

The response is 200 OK. AuthEndpoints requires a confirmed account by default, so the user can't sign in yet.

Confirm the email

Find the Confirm ada@example.com line in the app log. Copy the link. The log HTML-encodes & as &amp;, so change it back to &, then open the link:

curl -i "http://localhost:5265/identity/confirmEmail?userId=...&code=..."

The response is 200 OK.

Sign in

curl -i -c cookies.txt -b cookies.txt -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"Passw0rd!"}' \
  http://localhost:5265/identity/login

The response is 200 OK with a Set-Cookie: .AspNetCore.Identity.Application=... header. curl stores the cookie in cookies.txt.

Call the protected endpoint with the cookie:

curl -b cookies.txt http://localhost:5265/me

The response is {"email":"ada@example.com"}.

Sign out

Sign-out changes state on a cookie session, so it needs a CSRF token. Try it without one first:

curl -i -c cookies.txt -b cookies.txt -X POST http://localhost:5265/identity/logout

The response is 400 Bad Request. Get a token, then send it in the RequestVerificationToken header:

curl -c cookies.txt -b cookies.txt http://localhost:5265/identity/csrfToken
curl -i -c cookies.txt -b cookies.txt -X POST \
  -H "RequestVerificationToken: <csrfToken from the previous response>" \
  http://localhost:5265/identity/logout

The response is 200 OK. Call /me again and you get 401 Unauthorized.

What you built

You have an API with password registration, email confirmation, cookie sign-in, and CSRF-protected sign-out. The same host also maps account management, two-factor, password reset, passkeys, and step-up routes. See Endpoints for the full list.

Native and mobile

To issue tokens instead of a cookie, pass AuthEndpointsSignIn.IdentityBearer as the first argument:

builder.Services.AddAuthEndpoints<AppUser, AppDbContext>(AuthEndpointsSignIn.IdentityBearer, o =>
{
    o.Passkeys.ServerDomain = "example.com";
});

POST /identity/login then returns accessToken and refreshToken. See Bearer auth.

The bearer facade maps /identity/login, /identity/refresh, and /identity/logout. It does not map GET /identity/csrfToken. Passkeys stay on, and every passkey ceremony requires a CSRF token. A bearer token does not remove that requirement on anonymous requests such as passkey registration and passkey sign-in. To use passkeys with the bearer facade, map your own token endpoint:

using Microsoft.AspNetCore.Antiforgery;

app.MapGet("/identity/csrfToken", (IAntiforgery antiforgery, HttpContext context) =>
    Results.Ok(new { csrfToken = antiforgery.GetAndStoreTokens(context).RequestToken }));

The client must keep the antiforgery cookie from that response and send the token in the RequestVerificationToken header. If you do not use passkeys, set o.Passkeys.Enabled = false instead.

Roles

To use roles, call the three-type overload so AddRoles runs before AddEntityFrameworkStores:

builder.Services.AddAuthEndpoints<AppUser, AppRole, AppDbContext>(o =>
{
    o.Passkeys.ServerDomain = "example.com";
});

Your DbContext must be IdentityDbContext<AppUser, AppRole, TKey> or equivalent. Do not chain .AddRoles<TRole>() after the two-type overload.

Next steps