description: Set up JWT Bearer authentication with token service and authorization policies
Configure JWT Authentication Skill
Set up JWT Bearer authentication for NovaTune API.
Project Context
- Configuration:
src/NovaTuneApp/NovaTuneApp.ApiService/appsettings.json - Auth setup:
src/NovaTuneApp/NovaTuneApp.ApiService/Program.cs - Token service:
src/NovaTuneApp/NovaTuneApp.ApiService/Services/TokenService.cs
Steps
1. Add Configuration Section
Location: appsettings.json
{
"Jwt": {
"Issuer": "https://novatune.example",
"Audience": "novatune-api",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationMinutes": 60,
"SigningAlgorithm": "HS256"
}
}
Note: SigningKey should be in environment variable
JWT_SIGNING_KEYor secrets manager.
2. Create JWT Configuration Class
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Configuration/JwtSettings.cs
public class JwtSettings
{
public const string SectionName = "Jwt";
public required string Issuer { get; set; }
public required string Audience { get; set; }
public int AccessTokenExpirationMinutes { get; set; } = 15;
public int RefreshTokenExpirationMinutes { get; set; } = 60;
public string SigningAlgorithm { get; set; } = "HS256";
}
3. Create Token Service
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Services/TokenService.cs
public interface ITokenService
{
string GenerateAccessToken(ApplicationUser user);
string GenerateRefreshToken();
ClaimsPrincipal? ValidateToken(string token);
}
public class TokenService : ITokenService
{
private readonly JwtSettings _settings;
private readonly byte[] _signingKey;
public TokenService(IOptions<JwtSettings> settings, IConfiguration config)
{
_settings = settings.Value;
_signingKey = Encoding.UTF8.GetBytes(
config["JWT_SIGNING_KEY"]
?? throw new InvalidOperationException("JWT_SIGNING_KEY not configured"));
}
public string GenerateAccessToken(ApplicationUser user)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.UserId),
new(JwtRegisteredClaimNames.Email, user.Email),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new("status", user.Status.ToString())
};
// Add role claims
foreach (var role in user.Roles)
{
claims.Add(new Claim("roles", role.ToLowerInvariant()));
}
var key = new SymmetricSecurityKey(_signingKey);
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _settings.Issuer,
audience: _settings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpirationMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomBytes = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
public ClaimsPrincipal? ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(_signingKey);
try
{
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _settings.Issuer,
ValidateAudience = true,
ValidAudience = _settings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out _);
return principal;
}
catch
{
return null;
}
}
}
4. Configure Authentication in Program.cs
// Bind settings
builder.Services.Configure<JwtSettings>(
builder.Configuration.GetSection(JwtSettings.SectionName));
// Register token service
builder.Services.AddSingleton<ITokenService, TokenService>();
// Configure JWT authentication
var jwtSettings = builder.Configuration
.GetSection(JwtSettings.SectionName)
.Get<JwtSettings>()!;
var signingKey = Encoding.UTF8.GetBytes(
builder.Configuration["JWT_SIGNING_KEY"]
?? throw new InvalidOperationException("JWT_SIGNING_KEY not configured"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(signingKey),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
// After app.Build()
app.UseAuthentication();
app.UseAuthorization();
5. Configure Authorization Policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Listener", policy =>
policy.RequireAuthenticatedUser());
options.AddPolicy("Admin", policy =>
policy.RequireClaim("roles", "admin"));
options.AddPolicy("ActiveUser", policy =>
policy.Requirements.Add(new ActiveUserRequirement()));
});
6. Create ActiveUser Authorization Requirement
Location: src/NovaTuneApp/NovaTuneApp.ApiService/Authorization/ActiveUserRequirement.cs
public class ActiveUserRequirement : IAuthorizationRequirement { }
public class ActiveUserHandler : AuthorizationHandler<ActiveUserRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ActiveUserRequirement requirement)
{
var statusClaim = context.User.FindFirstValue("status");
if (statusClaim == UserStatus.Active.ToString())
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Required NuGet Packages
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
Environment Variables
| Variable | Description | Required |
|---|---|---|
JWT_SIGNING_KEY | Secret key for token signing (min 32 chars) | Yes |
Security Checklist
- Signing key is NOT in appsettings.json
- Signing key is at least 256 bits (32 characters)
-
ClockSkewis set toTimeSpan.Zero - Access token TTL is short (15 minutes)
-
jticlaim is included for revocation support - Token never logged (add to redaction policy)
Testing
[Fact]
public void GenerateAccessToken_IncludesRequiredClaims()
{
var user = new ApplicationUser
{
UserId = "01HQ3K...",
Email = "test@example.com",
Roles = ["Listener"]
};
var token = _tokenService.GenerateAccessToken(user);
var principal = _tokenService.ValidateToken(token);
principal.Should().NotBeNull();
principal!.FindFirstValue(JwtRegisteredClaimNames.Sub).Should().Be(user.UserId);
principal.FindFirstValue("roles").Should().Be("listener");
}