Practical guide for ASP.NET CORE 9 Identity authorization JWT and Cookies combined, custom User Id + Axios example for SPA
Introduction
I often see .NET developers struggling with implementing basic authentication using ASP.NET Core Identity. In this article, I have decided to provide a purely practical guide on implementing authentication. We will use a combined approach, allowing authentication with both JWT tokens and cookies when the [Authorize]
attribute is applied. This approach is beneficial for API authentication and Swagger login. Additionally, I will provide an example of using Axios for Cookies authentication in a SPA.
Backend
Let’s create a Web API project in .NET 9 and install necessary dependencies
dotnet new webapi -n AuthenticationExample --framework net9.0 --use-program-main
cd AuthenticationExample
nuget install Microsoft.AspNetCore.Authentication.JwtBearer
nuget install Microsoft.AspNetCore.Identity
nuget install Microsoft.EntityFrameworkCore.InMemory
nuget install Microsoft.EntityFrameworkCore
nuget install Microsoft.EntityFrameworkCore.Relational
nuget install Microsoft.AspNetCore.Identity.EntityFrameworkCore
nuget install Microsoft.AspNetCore.Authentication.Cookies
nuget install Microsoft.AspNetCore.Authentication.JwtBearer
We will use Guid as a default user id, so let’s add custom User model in Infrastructure/Models/User.cs
using Microsoft.AspNetCore.Identity;
namespace AuthenticationExample.Infrastructure.Models;
public class User : IdentityUser<Guid>
{
public User(Guid id, string email)
{
Id = id;
Email = email;
UserName = email; // Will set UserName to email for now.
}
public User()
{
}
}
Let’s Infrastructure/IApplicationContext.cs
using AuthenticationExample.Infrastructure.Models;
using Microsoft.EntityFrameworkCore;
namespace AuthenticationExample.Infrastructure;
public interface IApplicationContext
{
public DbSet<User> Users { get; }
public Task<int> SaveChangesAsync(CancellationToken ct = default);
}
Add Infrastructure/ApplicationContext.cs
using AuthenticationExample.Infrastructure.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace AuthenticationExample.Infrastructure;
public class ApplicationContext : IdentityDbContext<User, IdentityRole<Guid>, Guid>, IApplicationContext
{
public const string DefaultScheme = "authentication_example";
public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
ArgumentNullException.ThrowIfNull(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
}
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
return await base.SaveChangesAsync(ct);
}
}
Add Infrastructure/ServiceCollectionExtensions.cs and add InMemory database
using Microsoft.EntityFrameworkCore;
namespace AuthenticationExample.Infrastructure;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddEntityFramework(this IServiceCollection services, IConfiguration configuration)
{
return services
.AddScoped<IApplicationContext>(sp => sp.GetRequiredService<ApplicationContext>())
.AddDbContext<ApplicationContext>(options => options.UseInMemoryDatabase("db"));
}
}
Let’s install Swagger to show our API
nuget install Swashbuckle.AspNetCore.SwaggerGen
nuget install Swashbuckle.AspNetCore.SwaggerUI
Add JWT settings (Options) in Settings/JwtSettings.cs
using System.ComponentModel.DataAnnotations;
namespace AuthenticationExample.Settings;
public class JwtSettings
{
public const string Key = "JWT";
[Required(AllowEmptyStrings = false, ErrorMessage = "Please enter valid audience")]
public required string ValidAudience { get; init; }
[Required(AllowEmptyStrings = false, ErrorMessage = "Please enter valid issuer")]
public required string ValidIssuer { get; init; }
[Required(AllowEmptyStrings = false, ErrorMessage = "Please enter secret")]
public required string Secret { get; init; }
}
Add injection of settings in ServiceCollectionExtensions in Settings/ServiceCollectionExtensions.cs
namespace AuthenticationExample.Settings;
internal static class ServiceCollectionExtensions
{
public static void AddWebSettings(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<JwtSettings>()
.Bind(configuration.GetSection(JwtSettings.Key))
.ValidateDataAnnotations()
.ValidateOnStart();
}
}
Authorization services
Let’s add email sender that does nothing in Web/Auth/CustomNoOpEmailSender.cs. You can adjust this code later to use something like SendGrid to send your confirmation emails
using AuthenticationExample.Infrastructure.Models;
using Microsoft.AspNetCore.Identity;
namespace AuthenticationExample.Web.Auth;
public class CustomNoOpEmailSender : IEmailSender<User>
{
public Task SendConfirmationLinkAsync(User user, string email, string confirmationLink)
{
return Task.CompletedTask;
}
public Task SendPasswordResetLinkAsync(User user, string email, string resetLink)
{
return Task.CompletedTask;
}
public Task SendPasswordResetCodeAsync(User user, string email, string resetCode)
{
return Task.CompletedTask;
}
}
Add Web/Auth/TokenService.cs to generate new JWT tokens
using System.Security.Claims;
using System.Text;
using AuthenticationExample.Infrastructure.Models;
using AuthenticationExample.Settings;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
namespace AuthenticationExample.Web.Auth;
public class TokenService
{
private readonly JwtSettings _jwtSettings;
private readonly SymmetricSecurityKey _key;
private readonly UserManager<User> _userManager;
public TokenService(IOptions<JwtSettings> settings, UserManager<User> userManager)
{
_userManager = userManager;
_jwtSettings = settings.Value;
_key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtSettings.Secret));
}
public async Task<string> CreateToken(User user)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.UniqueName, user.Id.ToString())
};
claims.AddRange(await _userManager.GetClaimsAsync(user));
var creds = new SigningCredentials(_key,
SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1), // can be moved to configs as well
SigningCredentials = creds,
Issuer = _jwtSettings.ValidIssuer,
Audience = _jwtSettings.ValidAudience
};
var tokenHandler = new JsonWebTokenHandler();
return tokenHandler.CreateToken(tokenDescriptor);
}
}
Register services in Web/Auth/ServiceCollectionExtensions.cs
using AuthenticationExample.Infrastructure.Models;
using Microsoft.AspNetCore.Identity;
namespace AuthenticationExample.Web.Auth;
public static class ServiceCollectionExtensions
{
public static void AddAuthenticationServices(this IServiceCollection services)
{
services.AddSingleton<IEmailSender<User>, CustomNoOpEmailSender>();
services.AddScoped<PasswordHasher<User>>();
services.AddScoped<TokenService>();
}
}
Add AuthenticationController in Web/AuthenticationController.cs
using System.Security.Claims;
using AuthenticationExample.Infrastructure;
using AuthenticationExample.Infrastructure.Models;
using AuthenticationExample.Web.Auth;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace AuthenticationExample.Web;
[ApiController]
[Route("s/api/auth")]
public class AuthenticationController : ControllerBase
{
[HttpGet("me")]
[Authorize]
public async Task<MeDto> GetMe([FromServices] IApplicationContext ctx, CancellationToken ct)
{
var userId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
var user = await ctx.Users.FirstOrDefaultAsync(x => x.Id == userId, ct);
if (user == null)
{
throw new InvalidOperationException("User not found");
}
return new MeDto(user.Id, user.Email!);
}
[HttpPost("login/cookies")]
public async Task<IActionResult> LoginCookies(
[FromServices] UserManager<User> userManager,
[FromBody] LoginModel model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var user = await userManager.FindByEmailAsync(model.Email);
if (user == null)
{
return Unauthorized("Invalid email!");
}
var success = await userManager.CheckPasswordAsync(user, model.Password);
if (!success)
{
return Unauthorized("Invalid password!");
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(20),
IsPersistent = true,
IssuedUtc = DateTimeOffset.UtcNow,
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return Ok();
}
[HttpPost("token")]
public async Task<IActionResult> GenerateToken(
[FromServices] UserManager<User> userManager,
[FromServices] TokenService tokenService,
LoginModel model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var user = await userManager.FindByEmailAsync(model.Email);
if (user == null)
{
return Unauthorized("Invalid email!");
}
var success = await userManager.CheckPasswordAsync(user, model.Password);
if (!success)
{
return Unauthorized("Invalid password!");
}
return Ok(new LoginResponse
{
Email = user.Email!,
Token = await tokenService.CreateToken(user)
});
}
[HttpPost("register")]
public async Task<IActionResult> Register(
[FromServices] UserManager<User> userManager,
RegisterModel model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (model.Password != model.PasswordRepeat)
{
return Unauthorized("Passwords do not match!");
}
var user = await userManager.FindByEmailAsync(model.Email);
if (user != null)
{
return Unauthorized("Email address already exists!");
}
user = new User(Guid.NewGuid(), model.Email);
var result = await userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return Unauthorized("Username not found and/or password incorrect");
}
return Ok();
}
}
public record struct LoginModel(string Email, string Password);
public record struct RegisterModel(string Email, string Password, string PasswordRepeat);
public record struct LoginResponse(string Email, string Token);
public record struct MeDto(Guid Id, string Email);
Let’s put everything together in Program.cs
using AuthenticationExample.Infrastructure;
using AuthenticationExample.Infrastructure.Models;
using AuthenticationExample.Settings;
using AuthenticationExample.Web.Auth;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
namespace AuthenticationExample;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddWebSettings(builder.Configuration);
builder.Services.AddAuthenticationServices();
builder.Services.AddCors(options =>
{
options.AddPolicy(name: "AllowAllOrigins",
configurePolicy: policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddSwaggerGen(option =>
{
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter a valid token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "Bearer"
});
option.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
[]
}
});
});
builder.Services.AddEntityFramework(builder.Configuration);
builder.Services
.AddIdentity<User, IdentityRole<Guid>>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 8;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredUniqueChars = 1;
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedPhoneNumber = false;
options.SignIn.RequireConfirmedAccount = false;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthorization();
// Add dual authentication scheme to allow both JWT and Cookies auth.
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "DualScheme";
options.DefaultChallengeScheme = "DualScheme";
})
.AddPolicyScheme("DualScheme", "JWT or Cookie", options =>
{
options.ForwardDefaultSelector = context =>
{
// If we have Authorization header - use JWT, fallback to Cookies
string authorizationHeader = context.Request.Headers["Authorization"];
return !string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith("Bearer ")
? JwtBearerDefaults.AuthenticationScheme
: CookieAuthenticationDefaults.AuthenticationScheme;
};
})
.AddCookie(options =>
{
options.LoginPath = "/s/api/auth/login/cookies";
options.Cookie.Name = "AuthenticationExampleCookie";
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
// Consider security to prevent XSS
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JWT:ValidIssuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["JWT:ValidAudience"],
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"]!)
)
};
});
builder.Services.AddOpenApi();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html");
app.UseCors("AllowAllOrigins");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers().WithStaticAssets();
app.Run();
}
}
Frontend
Add simple html frontend in wwwroot/index.html
mkdir wwwroot
touch wwwroot/index.html
Edit index.html and add the following code with Axios and cookies auth
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios Cookies Auth</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<h1>Axios Authentication with Cookies</h1>
<button onclick="register()">Register</button>
<button onclick="login()">Login</button>
<button onclick="getUserData()">Get User Data</button>
<div id="output"></div>
<script>
// Set Axios to include credentials (cookies)
axios.defaults.withCredentials = true;
const API_BASE = 's/api/auth';
function register() {
axios.post(`${API_BASE}/register`, {
email: 'newuser@example.com',
password: 'password123',
passwordRepeat: 'password123'
}).then(response => {
document.getElementById('output').innerText = 'Registration successful';
}).catch(error => {
document.getElementById('output').innerText = 'Registration failed: ' + error.response.data;
});
}
function login() {
axios.post(`${API_BASE}/login/cookies`, {
email: 'newuser@example.com',
password: 'password123'
}).then(response => {
document.getElementById('output').innerText = 'Login successful';
}).catch(error => {
document.getElementById('output').innerText = 'Login failed: ' + error.response.data;
});
}
function getUserData() {
axios.get(`${API_BASE}/me`)
.then(response => {
document.getElementById('output').innerText = 'User Data: ' + JSON.stringify(response.data);
})
.catch(error => {
document.getElementById('output').innerText = 'Error fetching user data: ' + error.response.data;
});
}
</script>
</body>
</html>
Congrats! You’re all set.
My LinkedIn in case you want to connect:
GitHub with full example