ASP.NET Core Identity with JWT is the authentication stack behind most production .NET 10 applications that ship a Blazor Server UI and a REST API together. ASP.NET Core Identity manages user records, password hashing, and login flows, while JWT bearer tokens give your API a stateless way to authorize every incoming request. In this article you will build the complete setup end to end: Identity wired to EF Core, JwtBearer configured in Program.cs, tokens generated and signed, and both the Blazor Server pages and the REST endpoints protected by the same trust boundary.
Why ASP.NET Core Identity with JWT Is the Default Choice in .NET 10
ASP.NET Core ships two complementary pieces out of the box. Identity stores users and credentials in your own database through EF Core stores, which means you keep full control of the schema and can extend the AppUser class with extra columns such as TenantId or AvatarUrl. JWT bearer authentication, on the other hand, validates a cryptographically signed token on every API call without touching the database. That pairing gives you the durability of server-side user management plus the scalability of a stateless token.
There is also a practical reason this combination dominates .NET 10 projects. A Blazor Server application runs over SignalR, where the interactive UI needs a signed-in user identity, while mobile apps, background workers, and third-party consumers talk to your REST API over HTTP. One Identity store plus one JWT strategy serves both audiences without maintaining two separate authentication systems. When you buy a complete application like an Indotalent CRM or HRM, this is exactly the security wiring you will find inside.
It is worth being precise about what each piece does. Identity owns the user: who may log in, what password rules apply, how many failed attempts trigger lockout, and which roles or claims the user carries. The JWT bearer scheme owns the transport: it turns the verified identity into a signed token and later turns that token back into a ClaimsPrincipal. Neither piece is complete without the other, which is why you see them registered back to back in virtually every real .NET 10 starter.
The Complete Setup: ASP.NET Core Identity with JWT in Program.cs
The setup is compact in .NET 10. Add the Microsoft.AspNetCore.Identity.EntityFrameworkCore and Microsoft.AspNetCore.Authentication.JwtBearer NuGet packages, register Identity with AddIdentity, attach the EF Core stores, then chain AddJwtBearer with a TokenValidationParameters that matches the tokens you will generate:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services
.AddIdentity<AppUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 10;
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
builder.Services
.AddAuthentication(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:Key"]))
};
});
The AddIdentity call configures the user store, password policy, and lockout rules. The JwtBearer options tell the middleware which issuer and audience to trust, whether the token is still within its lifetime, and which signing key to verify against. In production you should store the signing key in user secrets, environment variables, or a key vault — never in appsettings.json.
Generating a Signed JWT with JwtSecurityTokenHandler
With the pipeline configured, the next piece is issuing tokens. The classic approach uses JwtSecurityTokenHandler from the System.IdentityModel.Tokens.Jwt package. Build a JwtSecurityToken with issuer, audience, expiry, and a small set of claims, sign it with an HMAC-SHA256 key, and write it to its compact string form:
public static string BuildToken(AppUser user, IConfiguration config)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(config["Jwt:Key"]));
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);
}
You normally call this method from a login endpoint after SignInManager validates the password. Because the token is self-contained, the API can authorize requests purely by verifying the signature and expiry — no session lookup and no database round trip on every call. Newer codebases often swap the handler for JsonWebTokenHandler from Microsoft.IdentityModel.JsonWebTokens, which is faster and allocation-light, but the validation settings stay the same.
Token Lifetime and the Refresh Token Companion
An expiry of one hour is a deliberate trade-off. Long-lived tokens are convenient but dangerous if stolen, so the standard companion pattern is a refresh token that can be revoked server-side. In practice you keep the access token in client memory while a hashed, rotated refresh token lives in the database. On every renewal the old refresh token is invalidated, so a stolen one is quickly detected and its whole family can be terminated. That rotation is the subject of the next article in this series.
Securing Blazor Server and REST Endpoints with One Token
Once Identity and JwtBearer are registered, authorization is entirely declarative. REST endpoints use the RequireAuthorization extension, and Blazor Server pages and components use the [Authorize] attribute or the AuthorizeView component. The same claims that arrive inside the JWT flow into HttpContext.User on every protected API call.
[Authorize]on a Razor component or page blocks anonymous access to the UI.RequireAuthorization("PolicyName")on a Minimal API endpoint enforces role or claim policies.<AuthorizeView>conditionally renders sections of a component based on the signed-in user.- Client-side REST calls attach the token with an
Authorization: Bearerheader.
That is the complete setup. The same token that unlocks your API can be carried by the Blazor client whenever it calls protected services, and this is precisely how Indotalent products wire Identity and JWT together across their CRM, HRM, and OMS codebases. The pattern scales from a single admin login to full role-based and claims-based authorization.
For finer control, combine the attribute with role and claim policies defined once in AddAuthorization. A component can require a role, an endpoint can demand a specific claim value, and the Razor layer can inspect HttpContext.User to adapt the UI to the signed-in identity. Authorization stays declarative from the moment the token is validated to the moment the UI renders.
Key Takeaways
- ASP.NET Core Identity with JWT pairs a database-backed user store with stateless bearer tokens
AddIdentity+AddEntityFrameworkStoresregisters the user store;AddJwtBearerregisters token validationJwtSecurityTokenHandlerwrites signed, expiring tokens carrying issuer, audience, and claims- The same authorization pipeline protects Minimal API endpoints and Blazor Server components
- Every Indotalent product ships this exact security foundation in real .NET 10 source code
FAQ
Do I need both ASP.NET Core Identity and JWT? Yes, for a complete solution — Identity manages users and passwords in your database, while JWT provides stateless authorization for your REST API and mobile clients.
Should the JWT signing key live in appsettings.json? No. Use user secrets, environment variables, or a key vault, because a leaked key lets an attacker forge valid tokens for any user.
Can a Blazor Server component use the JWT directly? The component relies on authentication state through [Authorize] and AuthorizeView; when the client makes REST calls, it attaches the bearer token to the request headers.
Is JwtSecurityTokenHandler still the right choice in .NET 10? It works perfectly and is the most documented path. Teams that care about throughput often switch to JsonWebTokenHandler from Microsoft.IdentityModel.JsonWebTokens without changing any validation options.