ASP.NET Core 10: Securing a .NET 10 Minimal API with ASP.NET Core Identity Built-in Endpoints, EF Core & Swagger
In this article, we will explore ASP.NET Core 10 Security for Minimal APIs with Built-In Security Enhancements
Why ASP.NET Core Identity's Built-in API Endpoints?
Starting with .NET 8, ASP.NET Core Identity ships a set of ready-made Minimal API endpoints for authentication. Instead of hand-rolling /register, /login, /refresh-token, and email-confirmation logic, you can call three simple extension methods:
AddIdentityApiEndpoints<TUser>()— This registers Identity's services configured for API scenarios (no cookies/redirects, JSON responses)..AddEntityFrameworkStores<TDbContext>()— This tells Identity to persist users, roles, and tokens using Entity Framework Core.app.MapIdentityApi<TUser>()— This maps the actual HTTP routes (register, login, refresh, confirmEmail, resendConfirmationEmail, forgotPassword, resetPassword, manage/info, manage/2fa) onto your route group.
Combined with Bearer tokens instead of cookies, this offfers a production-ready authentication surface for SPAs, mobile apps, and third-party API consumers in just a few lines of code.
Step 1: Wiring Up Identity Services
The code in Listing 1 shows the class that has all the Identity configuration lives in one extension method, AddApplicationIdentity, so that Program.cs stays clean:
public static class IdentityServiceExtensions
{
public static IServiceCollection AddApplicationIdentity(
this IServiceCollection services,
IConfiguration configuration,
IHostEnvironment environment)
{
var connectionString = configuration.GetConnectionString("IdentityDatabase");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException("Configure ConnectionStrings:IdentityDatabase for SQL Server.");
}
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
services.AddIdentityApiEndpoints<ApplicationUser>(options =>
{
// Skip email confirmation in Development since DevelopmentEmailSender only logs
// confirmation links instead of sending real emails, which would otherwise block login.
options.SignIn.RequireConfirmedEmail = !environment.IsDevelopment();
options.User.RequireUniqueEmail = true;
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
})
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.BearerScheme;
options.DefaultChallengeScheme = IdentityConstants.BearerScheme;
});
services.AddAuthorization();
services.AddOptions<BearerTokenOptions>(IdentityConstants.BearerScheme)
.Configure(options =>
{
options.BearerTokenExpiration = TimeSpan.FromMinutes(
configuration.GetValue("IdentityTokens:AccessTokenMinutes", 15));
options.RefreshTokenExpiration = TimeSpan.FromDays(
configuration.GetValue("IdentityTokens:RefreshTokenDays", 7));
})
.Validate(options => options.BearerTokenExpiration > TimeSpan.Zero,
"Access token lifetime must be positive.")
.Validate(options => options.RefreshTokenExpiration > options.BearerTokenExpiration,
"Refresh token lifetime must exceed the access token lifetime.")
.ValidateOnStart();
if (!environment.IsDevelopment())
{
throw new InvalidOperationException(
"Replace the Development email sender registration with a production IEmailSender<ApplicationUser>.");
}
services.AddTransient<IEmailSender<ApplicationUser>, DevelopmentEmailSender>();
return services;
}
}
Listing 1: The Identity Class
What this code actually does
AddDbContext<ApplicationDbContext>: This registers EF Core with SQL Server as the persistence provider for Identity's tables (users, roles, tokens).AddIdentityApiEndpoints<ApplicationUser>: This configures Identity for API use. Notably,RequireConfirmedEmailis toggled off only in Development, because the sample project doesn't send real emails locally (see theDevelopmentEmailSenderbelow) in Production, confirmation is enforced.- Bearer scheme as default:
DefaultAuthenticateScheme/DefaultChallengeSchemeare set toIdentityConstants.BearerScheme, which is exactly what.AddBearerToken()configures behind the scenes forMapIdentityApitokens are validated from theAuthorization: Bearer <token>header instead of cookies. BearerTokenOptions: The customizes access-token and refresh-token lifetimes fromappsettings.json, with validation rules enforced on startup (ValidateOnStart) so misconfiguration fails immediately rather than silently at runtime.- Email sender registration: The throws in non-Development environments as a deliberate guardrail, forcing a real
IEmailSender<ApplicationUser>implementation (e.g. SendGrid, SMTP) to be wired up before shipping to Production.
Step 2: The Application User & DbContext
The code in Listing 2 shows that, rather than using the framework's own IdentityUser, the project defines a custom ApplicationUser so tat we can add extra profile fields without breaking changes:
public sealed class ApplicationUser : IdentityUser
{
public string? DisplayName { get; set; }
}
public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: IdentityDbContext<ApplicationUser>(options)
{
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>()
.Property(user => user.DisplayName)
.HasMaxLength(100);
}
}
Listing 2: The ApplicationUser and ApplicationDbContext classes
The ApplicationDbContext inherits IdentityDbContext<ApplicationUser>, which already defines all the Identity tables like AspNetUsers, AspNetRoles, AspNetUserTokens, etc. The override is just to add a column-length constraint for the new DisplayName property.
Step 3: Program.cs — Put Everything Together
using Core10_JWTApp.Identity;
using Core10_JWTApp.Patients;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
var builder = WebApplication.CreateBuilder(args);
// Registers OpenAPI document generation, and adds a Bearer security scheme so tokens
// can be supplied via the Swagger UI "Authorize" button.
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Enter the access token returned by POST /auth/login."
};
document.Security ??= new List<OpenApiSecurityRequirement>();
document.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", document)] = new List<string>()
});
return Task.CompletedTask;
});
});
// Registers EF Core DbContext, ASP.NET Core Identity API endpoints, and bearer token authentication.
builder.Services.AddApplicationIdentity(builder.Configuration, builder.Environment);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
// Ensure the Identity database/schema exists on startup (Development only, no migrations required).
await using var scope = app.Services.CreateAsyncScope();
var database = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await database.Database.EnsureCreatedAsync();
// Expose the OpenAPI JSON document for local testing/tools.
app.MapOpenApi();
// Serve the Swagger UI page at /swagger, so all endpoints can be browsed and tested.
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "Core10_JWTApp v1");
options.RoutePrefix = "swagger";
});
}
app.UseHttpsRedirection();
app.UseAuthentication(); // Resolves the caller's identity (bearer token) from the request.
app.UseAuthorization(); // Enforces RequireAuthorization() rules on endpoints.
// Groups all Identity endpoints (register, login, refresh, confirmEmail, manage/info, etc.)
// under the "/auth" prefix.
var authentication = app.MapGroup("/auth").WithTags("Authentication");
authentication.MapIdentityApi<ApplicationUser>();
// Groups the custom Patients API under "/api".
var api = app.MapGroup("/api").WithTags("Patients");
api.MapPatientEndpoints();
app.Run();
Listing 3: Program.cs to put all together.
What Program.cs code does?
- OpenAPI + Bearer scheme: .NET 9/10's built-in OpenAPI generator doesn't add a security scheme automatically, so a document transformer is used to inject a
BearerHTTP security scheme into the generated spec. This is used to have the Swagger UI with an Authorize button that attaches the token to every secured request. EnsureCreatedAsync(): in Development, creates the database and tables if they don't already exist this is a quick way to bootstrap without running EF Core migrations.MapOpenApi()+UseSwaggerUI(): the framework generates the raw OpenAPI JSON at/openapi/v1.json; Swashbuckle'sSwaggerUImiddleware then renders an interactive HTML page at/swaggerthat reads that JSON.- Middleware order matters:
UseAuthentication()must run beforeUseAuthorization()the authentication identifies who is calling, the authorization decides what they're allowed to do. MapIdentityApi<ApplicationUser>(): This is the single call that maps all the ASP.NET Core 10 built-in Identity routes:/register,/login,/refresh,/confirmEmail,/resendConfirmationEmail,/forgotPassword,/resetPassword, and/manage/info(for reading/updating email & password), all prefixed with/auth.MapPatientEndpoints(): registers the custom, secured Patients API under/api.
Step 4: A Secured "Patients" Resource
To demonstrate a real protected resource beyond authentication itself, a small Patients API was added: an entity class, an in-memory sample dataset, and Minimal API endpoints that require a valid bearer token.
The Patient entity
public sealed class Patient
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public string Gender { get; set; } = string.Empty;
public string Diagnosis { get; set; } = string.Empty;
public string ContactNumber { get; set; } = string.Empty;
public DateOnly AdmissionDate { get; set; }
}
Listing 4: The Patient Entity
A plain POCO used both as the in-memory sample data shape and the JSON contract returned by the API. Sample data lives in a static PatientStore class so no database is required for this demo endpoint.
public static class PatientEndpoints
{
public static RouteGroupBuilder MapPatientEndpoints(this RouteGroupBuilder group)
{
var patients = group.MapGroup("/patients").RequireAuthorization();
patients.MapGet("", GetPatients).WithName("GetPatients");
patients.MapGet("/{id:int}", GetPatientById).WithName("GetPatientById");
return group;
}
private static Ok<IReadOnlyList<Patient>> GetPatients() =>
TypedResults.Ok(PatientStore.SamplePatients);
private static Results<Ok<Patient>, NotFound> GetPatientById(int id)
{
var patient = PatientStore.SamplePatients.FirstOrDefault(p => p.Id == id);
return patient is null ? TypedResults.NotFound() : TypedResults.Ok(patient);
}
}
Listing 5: The secured endpoints
The key line is .RequireAuthorization() on the route group, it forces every route under /patients to reject anonymous callers with 401 Unauthorized unless a valid Bearer token that is obtained from /auth/login is attached to the request. The TypedResults is used instead of plain Results for strongly-typed, OpenAPI-friendly responses like 200 OK or 404 Not Found are declared right in the method signature.
Following Video shows the execution:
Summary with Important Points
- ASP.NET Core Identity's built-in API endpoints eliminate boilerplate auth code — registration, login, refresh tokens, and email confirmation come for free.
AddIdentityApiEndpoints<TUser>().AddEntityFrameworkStores<TDbContext>()plusapp.MapIdentityApi<TUser>()is all it takes to stand up a token-based auth API.- Bearer scheme (instead of cookies) is the right choice for APIs consumed by SPAs, mobile clients, or third parties.
RequireAuthorization()on a route group is the simplest way to lock down a whole feature (like the Patients API) behind authentication.- A document transformer lets you extend the built-in OpenAPI generator (e.g. adding a Bearer security scheme) without needing a full third-party Swagger generator.