ASP.NET Core Identity with JWT fits vertical slice architecture surprisingly well, because authentication is itself a business capability. In a vertical slice application every feature — Register, Login, Refresh Token, Forgot Password — becomes a small, self-contained slice with its own endpoint, DTOs, and handler, grouped under a Features/Auth folder. This article shows how to design that auth slice in a .NET 10 vertical slice app, and how it connects to the slices that come after it.
Why ASP.NET Core Identity with JWT Fits Vertical Slices
Vertical slice architecture organizes code by business capability instead of technical layer. Instead of scattering authentication logic across Controllers/, Services/, and Repositories/, every auth flow lives in one place. The login slice owns its command, its validation, and its handler; the refresh slice owns its own rotation logic. When you change how login works, you open one folder and you are done.
This matches how Identity is used in practice. Identity is a framework, not an architecture — it gives you UserManager, SignInManager, and token services, but it does not tell you where login logic belongs. Vertical slicing answers that question: each security flow is a slice that leans on the framework services without burying them in a generic service layer.
Contrast this with a layered project where login logic sits in an AccountService that also handles profile updates and password resets, and where the API controller, the DTOs, and the validation live in three different folders. VSA keeps each flow local, so a change to refresh token rotation never risks breaking registration, and a new developer can answer "how does login work?" by reading one small folder instead of grepping the whole solution.
Structuring ASP.NET Core Identity with JWT Inside an Auth Slice
A clean auth slice looks like any other slice folder. One subfolder per feature, each containing the endpoint, request DTOs, and a MediatR handler. Cross-cutting pieces such as the token service are shared infrastructure that every slice can reference:
Features/
Auth/
Register/
Register.cs # command + handler
RegisterValidator.cs
Login/
Login.cs # command + handler
LoginResponse.cs
RefreshToken/
RefreshToken.cs
ForgotPassword/
ForgotPassword.cs
AuthTokenService.cs # shared JWT builder
Customers/
CreateCustomer/
ListCustomers/
Orders/
CreateOrder/
The auth slice sits alongside every other feature in the application. There is no separate "security layer" to jump into — the register flow, the login flow, and the refresh flow are all first-class citizens with the same structure as CreateOrder or ListCustomers.
Two rules keep the folder clean. First, a slice may use framework services like UserManager or SignInManager, but it must not depend on another feature slice — Login never calls into Register's code directly. Second, shared infrastructure such as AuthTokenService is deliberately small and stateless; if it starts growing validation rules or UI concerns, those belong in a slice. The folder structure stays readable because each slice is an island with one job.
The boundary between the auth slice and the rest of the app is the token itself. Every other slice sees only a ClaimsPrincipal built from the JWT, so order creation never touches Identity directly. That decoupling is why you can swap token lifetimes or add a second factor without editing a single business slice.
An Auth Slice Code Walkthrough
The login slice is the best example, because it shows how thin a slice can be. The command carries the input, and the handler uses SignInManager to validate credentials before asking the shared token service to build a JWT:
// Features/Auth/Login/Login.cs
public sealed record LoginCommand(string Email, string Password) : IRequest<LoginResponse>;
public sealed class LoginHandler : IRequestHandler<LoginCommand, LoginResponse>
{
readonly SignInManager<AppUser> _signIn;
readonly AuthTokenService _tokens;
public LoginHandler(SignInManager<AppUser> signIn, AuthTokenService tokens)
{
_signIn = signIn;
_tokens = tokens;
}
public async Task<LoginResponse> Handle(LoginCommand cmd, CancellationToken ct)
{
var user = await _signIn.UserManager.FindByEmailAsync(cmd.Email);
if (user is null ||
!await _signIn.UserManager.CheckPasswordAsync(user, cmd.Password))
return LoginResponse.Failed("Invalid credentials");
await _signIn.SignInAsync(user, isPersistent: false);
return LoginResponse.Ok(_tokens.BuildAccessToken(user));
}
}
// Features/Auth/Login/LoginEndpoint.cs
public static class LoginEndpoint
{
public static void Map(IEndpointRouteBuilder app) =>
app.MapPost("/api/auth/login",
async (LoginCommand cmd, IMediator mediator) =>
await mediator.Send(cmd));
}
The endpoint file and the handler file stay tiny, which is exactly what makes vertical slices easy to read and easy to test. Validation lives in a fluent validator beside the command, and errors return a consistent shape instead of leaking Identity exceptions to the API consumer.
The endpoint class is mapped once from the auth slice's own extension method, so Program.cs stays a thin composition root: it registers global services and then calls AuthEndpoints.Map(app) alongside the other feature maps. That keeps the composition root readable even as the application grows to dozens of slices.
The Register Slice Follows the Same Shape
Registration is equally small. The handler calls UserManager.CreateAsync, sends a confirmation email through a notification slice, and returns the new user id — no business rules beyond the ones already enforced by the password validator. Because validation lives beside the command, adding a rule such as an approved-domains check is a change to one validator file, not a tour through a services project.
Cross-Slice Concerns: Policies, Claims, and Middleware
Not everything belongs in the auth slice. Global infrastructure — Identity registration, JwtBearer validation, and the authorization policies — stays in Program.cs exactly as in a non-VSA app, because it is configuration, not business logic. The auth slice adds and validates tokens; the rest of the app consumes them through roles and claims:
- Register Identity, EF Core stores, and JwtBearer once in
Program.cs. - Define authorization policies there too, and reference them from slices with
RequireAuthorization. - Embed roles and claims in the token at login time inside the auth slice.
- Keep feature validation in the slice; keep the token service shared and stateless.
With that split, a feature slice like Orders/CreateOrder stays fully focused on its business rule while still benefiting from authentication. This is the exact structure used across Indotalent products, where CRM, HRM, and OMS all combine ASP.NET Core Identity with JWT and vertical slice architecture in one .NET 10 codebase.
Testing follows the same boundaries. Each auth handler is an integration test against a real or in-memory database: register a user, log in, call the refresh endpoint, and assert that the old refresh token is revoked. Because the handler is small and its dependencies are injected, the test stays focused on the flow it names — the test file mirrors the feature file, which is the quiet advantage of vertical slicing.
Key Takeaways
- ASP.NET Core Identity with JWT becomes an auth slice: one folder per auth feature
- Register, Login, Refresh Token, and Forgot Password each get endpoint, DTOs, and handler
- Global concerns — Identity registration, JwtBearer, policies — stay in Program.cs
- Auth slices are as thin and testable as any other vertical slice
- Indotalent products demonstrate ASP.NET Core Identity with JWT inside real VSA .NET 10 code