DEV Community

Cover image for ASP.NET Core JWT and cookie authentication in one project
GANESH KUMAR
GANESH KUMAR

Posted on

ASP.NET Core JWT and cookie authentication in one project

My store has two kinds of clients. Shoppers use the website, where a normal Identity login cookie is the right choice. Mobile apps use a REST API, where a cookie is awkward and a bearer token is the norm.

I didn't want two backends, two user tables or two copies of the business logic. Both live in one ASP.NET Core project and share the same services. This is how eCommerceMVCCore, my ASP.NET Core e-commerce platform, does it, with simplified code.

Website requests use the Identity cookie and redirect to login; API requests use JWT bearer and get a 401

1. Keep Identity cookies as the default

AddIdentity (or AddDefaultIdentity) already registers cookie authentication and makes it the default scheme. Leave that alone. The website keeps working exactly as before.

2. Add JWT bearer as a second scheme

builder.Services.AddAuthentication()
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        var key = builder.Configuration["Jwt:Key"]!;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)
        };
    });
Enter fullscreen mode Exit fullscreen mode

Calling AddAuthentication() with no arguments adds the scheme without changing the default.

3. Tell the API controllers to use JWT

Put the scheme on a base controller, so every API controller gets it:

[ApiController]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public abstract class ApiControllerBase : ControllerBase { }
Enter fullscreen mode Exit fullscreen mode

This also fixes the most common bug in mixed apps. With the cookie scheme, an unauthenticated request gets a 302 redirect to the login page. A mobile app expects a 401, and gets an HTML login page instead. With the JWT scheme on the API controllers, the challenge returns a clean 401.

4. Issue tokens with the same Identity users

The token endpoint checks the password with Identity's SignInManager, then builds a token with the user's roles:

[AllowAnonymous]
[HttpPost("/api/Token")]
public async Task<IActionResult> Token(LoginRequest request)
{
    var user = await userManager.FindByNameAsync(request.UserName);
    if (user is null) return Unauthorized();

    var result = await signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
    if (!result.Succeeded) return Unauthorized();

    var claims = new List<Claim>
    {
        new(JwtRegisteredClaimNames.Sub, user.Id),
        new(ClaimTypes.Name, user.UserName!)
    };
    foreach (var role in await userManager.GetRolesAsync(user))
        claims.Add(new Claim(ClaimTypes.Role, role));

    var creds = new SigningCredentials(
        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!)),
        SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: config["Jwt:Issuer"],
        audience: config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddHours(2),
        signingCredentials: creds);

    return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
Enter fullscreen mode Exit fullscreen mode

Note lockoutOnFailure: true. A token endpoint is a password-guessing target, so it should respect the same lockout rules as the login page.

See the API: the ASP.NET Core REST API in eCommerceMVCCore covers catalog, cart, orders, PayPal and account endpoints, with a Postman collection.

5. Share the services, not the controllers

The MVC CartController and the API CartController are thin. Both call the same ICartService, IOrderService and pricing code. The rules for prices, stock and promo codes live in one place, so the website and the app can't disagree about what an order costs.

Things to get right before production

  • The signing key. Use a long random secret, kept out of appsettings.json in source control. Environment variables or a secret store are both fine. A placeholder key in production means anyone can mint admin tokens.
  • Token lifetime. Short-lived tokens limit the damage of a leaked one. If you need long sessions in the app, add refresh tokens rather than making access tokens last for weeks.
  • CORS. A native mobile app doesn't need CORS, but a browser-based client on another domain does. Allow only the origins you actually use.

Testing it

I ship a Postman collection with the project. The first request calls POST /api/Token and saves the token to a variable, and every other request sends it as Authorization: Bearer .... It's the quickest way to check that the API returns 401s, not login pages.

FAQ

Can ASP.NET Core use cookie and JWT authentication at the same time?

Yes. Register both schemes, keep the cookie as the default, and name the JWT scheme on your API controllers. For more complex rules, a policy scheme with ForwardDefaultSelector can choose the scheme per request.

Why does my ASP.NET Core API return a login page instead of 401?

Because the cookie scheme's challenge is a redirect to the login page. Authenticate API controllers with the JWT bearer scheme, and its challenge returns a plain 401.

Where should I store the JWT signing key?

In environment variables, a secret store or a key vault, never in source control. Use a long random key: at least 256 bits for HS256. The go-live checklist in eCommerceMVCCore includes replacing the placeholder key.

Get the source code

This setup, with catalog, cart, order, PayPal and account endpoints, is part of eCommerceMVCCore. Get the ASP.NET Core 10 source code with the JWT REST API.

Do you keep your API in the same project as the website, or split it out? Tell me in the comments.

Top comments (0)