ASP.NET Core 10 Authentication Guide — JWT, Passkeys, Cookies & More
Authentication is the most critical part of any backend application. Get it wrong and your users' data is at risk. Get it right and your app is production-ready from day one.
ASP.NET Core 10 brings some of the biggest authentication upgrades in years — built-in Passkey (WebAuthn) support, improved cookie authentication for API endpoints, new Identity metrics, and cleaner JWT setup. In this guide I will walk you through every authentication method with working C# code so you can implement them in your own projects today.
Table of Contents
- Authentication vs Authorization — the difference
- JWT Bearer Authentication — full setup
- Cookie Authentication — and the .NET 10 breaking change
- ASP.NET Core Identity — production setup
- Passkeys (WebAuthn) — new in .NET 10
- Two-Factor Authentication (2FA / TOTP)
- Role-Based and Claims-Based Authorization
- New Identity Metrics in .NET 10
- Security best practices checklist
01 — Fundamentals Authentication vs Authorization
Before writing a single line of code, every backend developer must understand the difference between these two terms. They are not the same thing and confusing them causes serious security holes.
- Authentication — verifies who you are. "Is this person really Adnan?" It checks identity using credentials like passwords, tokens, or biometrics.
- Authorization — verifies what you are allowed to do. "Can Adnan delete posts?" It checks permissions after identity is confirmed.
In ASP.NET Core 10 the flow is always: Authentication first → Authorization second. You cannot authorize someone whose identity you have not confirmed.
Real-world example
Your passport proves who you are (authentication). The visa stamp in your passport says which countries you can enter (authorization). Same pattern applies in your API.
02 — Most Common JWT Bearer Authentication
JWT (JSON Web Token) is the most widely used authentication method for REST APIs and microservices. When a user logs in, your server issues a signed JWT token. The client sends this token in every subsequent request, and your API validates it without touching the database.
Step 1 — Install the NuGet package
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Step 2 — Configure JWT in Program.cs
C#// Program.cs — ASP.NET Core 10 var builder = WebApplication.CreateBuilder(args); // Add JWT Authentication builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!)) }; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); // Always before UseAuthorization app.UseAuthorization(); app.Run();
Step 3 — Generate a JWT token on login
C#// AuthService.cs — generate JWT token public class AuthService { private readonly IConfiguration _config; public AuthService(IConfiguration config) => _config = config; public string GenerateToken(string userId, string email, string role) { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Email, email), new Claim(ClaimTypes.Role, role), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"]!)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddHours(1), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } } // Minimal API login endpoint app.MapPost("/api/auth/login", (LoginRequest req, AuthService auth) => { // Validate user credentials against your database here var token = auth.GenerateToken("user-123", req.Email, "User"); return Results.Ok(new { token }); }); // Protected endpoint app.MapGet("/api/profile", (ClaimsPrincipal user) => { var email = user.FindFirst(ClaimTypes.Email)?.Value; return Results.Ok(new { email }); }).RequireAuthorization();
appsettings.json — JWT configuration
JSON{ "Jwt": { "Issuer": "https://codewithadnanshaukat.blogspot.com", "Audience": "YourAppUsers", "SecretKey": "YourSuperSecretKey_AtLeast32CharsLong!" } }
⚠ Security warning
Never store your JWT SecretKey in appsettings.json in production. Use environment variables or Azure Key Vault. The key should be at least 32 characters long and randomly generated.
03 — Cookie Auth Cookie Authentication & The .NET 10 Breaking Change
Cookie authentication is used for traditional web applications with server-rendered pages. After login, the server issues an encrypted cookie that the browser sends automatically with every request.
C#// Program.cs — Cookie Authentication setup builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/account/login"; options.LogoutPath = "/account/logout"; options.AccessDeniedPath = "/account/access-denied"; options.ExpireTimeSpan = TimeSpan.FromHours(8); options.SlidingExpiration = true; options.Cookie.HttpOnly = true; // Prevents JavaScript access options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPS only options.Cookie.SameSite = SameSiteMode.Strict; }); // Sign in a user after credential validation app.MapPost("/account/login", async (LoginRequest req, HttpContext ctx) => { // Validate credentials against your database here... var claims = new List<Claim> { new Claim(ClaimTypes.Name, req.Email), new Claim(ClaimTypes.Role, "User") }; var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); return Results.Redirect("/"); });
The .NET 10 Breaking Change — API endpoints now return 401/403
This is an important change that will affect you if you are mixing cookie auth with API endpoints. In previous versions, unauthenticated requests to API endpoints got a 302 Redirect to the login page — even from API clients. This was a well-known annoyance.
In .NET 10, when IApiEndpointMetadata is present on an endpoint, the cookie authentication handler now correctly returns:
- 401 Unauthorized — for unauthenticated requests
- 403 Forbidden — for authenticated but unauthorized requests
This is correct REST API behaviour. However if you want to keep the old redirect behaviour, you can override it:
C#// .NET 10 — override to keep redirect behaviour (optional) .AddCookie(options => { options.Events.OnRedirectToLogin = context => { context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; }; options.Events.OnRedirectToAccessDenied = context => { context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; }; });
04 — Identity ASP.NET Core Identity — Production Setup
ASP.NET Core Identity is Microsoft's full membership system — it handles user registration, login, password hashing, email confirmation, lockout, 2FA, and more. It is the right choice for any app that manages its own users.
Install packages
dotnet new webapp -n MySecureApp
cd MySecureApp
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Setup Identity with EF Core
C#// AppUser.cs — extend IdentityUser with custom fields public class AppUser : IdentityUser { public string FullName { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } // AppDbContext.cs public class AppDbContext : IdentityDbContext<AppUser> { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } } // Program.cs — wire everything up builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); builder.Services.AddIdentity<AppUser, IdentityRole>(options => { // Password policy options.Password.RequiredLength = 8; options.Password.RequireUppercase = true; options.Password.RequireDigit = true; options.Password.RequireNonAlphanumeric = true; // Lockout policy — important for brute force protection options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); options.Lockout.AllowedForNewUsers = true; // Require confirmed email before login options.SignIn.RequireConfirmedEmail = true; }) .AddEntityFrameworkStores<AppDbContext>() .AddDefaultTokenProviders();
05 — New in .NET 10 Passkeys (WebAuthn) — Passwordless Authentication
This is the most exciting authentication feature in .NET 10. Passkeys let users sign in using their device's biometrics (fingerprint, Face ID) or a hardware security key — no password required. They are built on the WebAuthn and FIDO2 standards and are completely phishing-resistant.
Why passkeys matter
Passkeys cannot be phished, stolen in data breaches (only public keys are stored), or guessed. A real developer who built passkey auth for a healthcare portal reported zero forgotten password support tickets after switching. That alone saves hours of developer time every week.
Configure passkeys in .NET 10
C#// Program.cs — configure passkey options in .NET 10 builder.Services.Configure<IdentityPasskeyOptions>(options => { // Your domain (Relying Party ID) — must match your website domain options.ServerDomain = "codewithadnanshaukat.blogspot.com"; // How long the user has to complete the passkey ceremony options.AuthenticatorTimeout = TimeSpan.FromMinutes(3); // Size of the cryptographic challenge in bytes options.ChallengeSize = 64; }); // The Blazor Web App template includes passkey UI out of the box. // For custom apps, you handle two endpoints: // 1. Registration — generate challenge, browser calls navigator.credentials.create() app.MapPost("/passkey/register/begin", async ( HttpContext ctx, UserManager<AppUser> userManager) => { var user = await userManager.GetUserAsync(ctx.User); var options = await userManager.CreatePasskeyCreationOptionsAsync(user!); return Results.Ok(options); }).RequireAuthorization(); // 2. Login — verify the passkey signature from the device app.MapPost("/passkey/login", async ( PasskeyAssertion assertion, SignInManager<AppUser> signInManager) => { var result = await signInManager.PasskeySignInAsync(assertion); return result.Succeeded ? Results.Ok() : Results.Unauthorized(); });
Current limitation
In .NET 10, the Blazor Web App template still requires users to set a password during initial registration before adding a passkey. Passkeys are an additional login method, not a complete password replacement yet in the default templates.
06 — 2FA Two-Factor Authentication (TOTP)
Two-factor authentication adds a second verification step after the password. In ASP.NET Core Identity, TOTP (Time-based One-Time Password) is the most common 2FA method — it works with apps like Google Authenticator and Microsoft Authenticator.
C#// Enable 2FA — generate a TOTP authenticator key for the user app.MapGet("/account/enable-2fa", async ( HttpContext ctx, UserManager<AppUser> userManager) => { var user = await userManager.GetUserAsync(ctx.User); await userManager.ResetAuthenticatorKeyAsync(user!); var key = await userManager.GetAuthenticatorKeyAsync(user!); // Show this key to the user as a QR code for their authenticator app return Results.Ok(new { AuthenticatorKey = key }); }).RequireAuthorization(); // Verify 2FA code and complete login app.MapPost("/account/verify-2fa", async ( TwoFactorRequest req, SignInManager<AppUser> signInManager) => { var result = await signInManager.TwoFactorAuthenticatorSignInAsync( req.Code, isPersistent: false, rememberClient: false ); return result.Succeeded ? Results.Ok(new { message = "Login successful" }) : Results.BadRequest(new { message = "Invalid code" }); }); public record TwoFactorRequest(string Code);
07 — Authorization Role-Based and Claims-Based Authorization
Once authentication is confirmed, authorization decides what the user can do. ASP.NET Core 10 supports three authorization approaches — roles, claims, and policies.
C#// Program.cs — define authorization policies builder.Services.AddAuthorization(options => { // Role-based policy options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); // Claims-based policy options.AddPolicy("CanPublish", policy => policy.RequireClaim("Permission", "Publish")); // Combined policy — must be authenticated AND have a role options.AddPolicy("SeniorDev", policy => policy.RequireAuthenticatedUser() .RequireRole("Developer") .RequireClaim("YearsExperience", "5")); }); // Blog API example — different access levels // Anyone can read posts app.MapGet("/api/posts", () => Results.Ok("All posts")); // Only logged-in users can comment app.MapPost("/api/posts/{id}/comments", (CommentRequest req) => Results.Ok("Comment added")) .RequireAuthorization(); // Only admins can create or delete posts app.MapPost("/api/posts", (PostRequest req) => Results.Ok("Post created")) .RequireAuthorization("AdminOnly"); app.MapDelete("/api/posts/{id}", (int id) => Results.Ok("Post deleted")) .RequireAuthorization("AdminOnly");
08 — New in .NET 10 Identity Metrics & Observability
ASP.NET Core 10 adds built-in authentication and authorization metrics — something developers have been asking for since Identity was first released. You can now monitor these events out of the box:
- User management — new user registrations, password changes, role assignments
- Login/session tracking — login attempts, successful sign-ins, sign-outs, 2FA usage
- Request duration — authenticated request duration visible in the Aspire dashboard
- Auth/authz events — authorization successes and failures per endpoint
C#// .NET 10 — metrics are available automatically under this meter // Microsoft.AspNetCore.Identity // Connect to .NET Aspire or Prometheus to see: // - aspnetcore.identity.user_management.count // - aspnetcore.identity.login.count // - aspnetcore.identity.login.duration // - aspnetcore.authorization.requests (succeeded/failed) // To expose metrics via OpenTelemetry in your app: builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() .AddMeter("Microsoft.AspNetCore.Identity") .AddPrometheusExporter(); // or use Aspire dashboard });
09 — Checklist Security Best Practices
Before shipping any authentication system to production, go through this checklist:
Store JWT secret keys in environment variables — never in source code or appsettings.json
Enable account lockout — protects against brute-force password attacks
Use HTTPS only — set CookieSecurePolicy.Always and enforce HTTPS in Kestrel
Set short JWT expiry — 15–60 minutes, use refresh tokens for longer sessions
Enable 2FA for admin accounts — at minimum, all admin users must use TOTP
Set HttpOnly and SameSite on cookies — prevents XSS and CSRF attacks
Require email confirmation — set RequireConfirmedEmail = true in Identity options
Monitor auth metrics — use .NET 10's built-in Identity metrics to detect anomalies
Consider passkeys — for any new user-facing app, offer WebAuthn as a login option in .NET 10
Final Thoughts
.NET 10 is the best version of ASP.NET Core for building secure applications. Passkeys, proper API status codes, built-in metrics, and a rock-solid Identity system give you everything you need without third-party dependencies. Start with JWT for your APIs, add Identity for user management, and offer passkeys to give your users the best login experience available today.
Written by
Adnan Shaukat
Backend Developer with 5+ years in .NET, Python, and REST APIs. Writing daily at codewithadnanshaukat.blogspot.com
Comments
Post a Comment