Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions src/SimpleAuthentication/JwtBearer/JwtBearerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,35 @@

namespace SimpleAuthentication.JwtBearer;

internal class JwtBearerService(IOptions<JwtBearerSettings> jwtBearerSettingsOptions) : IJwtBearerService
/// <summary>
/// Default implementation of <see cref="IJwtBearerService"/> that provides JWT Bearer token generation and validation.
/// </summary>
/// <param name="jwtBearerSettingsOptions">The JWT Bearer settings.</param>
public class JwtBearerService(IOptions<JwtBearerSettings> jwtBearerSettingsOptions) : IJwtBearerService
{
private readonly JwtBearerSettings jwtBearerSettings = jwtBearerSettingsOptions.Value;
/// <summary>
/// Gets the JWT Bearer settings used by this service.
/// </summary>
protected JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions.Value;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this type is now part of the public API surface, the primary-constructor parameter should be validated. As written, passing a null IOptions will result in a NullReferenceException when initializing JwtBearerSettings; prefer throwing ArgumentNullException (and optionally validate jwtBearerSettingsOptions.Value as well) to fail fast with a clearer error.

Suggested change
protected JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions.Value;
protected JwtBearerSettings JwtBearerSettings { get; } =
jwtBearerSettingsOptions is null
? throw new ArgumentNullException(nameof(jwtBearerSettingsOptions))
: jwtBearerSettingsOptions.Value;

Copilot uses AI. Check for mistakes.

public Task<string> CreateTokenAsync(string userName, IList<Claim>? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null)
/// <inheritdoc />
public virtual Task<string> CreateTokenAsync(string userName, IList<Claim>? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null)
{
claims ??= [];
claims.Update(jwtBearerSettings.NameClaimType, userName);
claims.Update(JwtBearerSettings.NameClaimType, userName);
claims.Update(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString());

var now = DateTime.UtcNow;

var securityTokenDescriptor = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(claims, jwtBearerSettings.SchemeName, jwtBearerSettings.NameClaimType, jwtBearerSettings.RoleClaimType),
Issuer = issuer ?? jwtBearerSettings.Issuers?.FirstOrDefault(),
Audience = audience ?? jwtBearerSettings.Audiences?.FirstOrDefault(),
Subject = new ClaimsIdentity(claims, JwtBearerSettings.SchemeName, JwtBearerSettings.NameClaimType, JwtBearerSettings.RoleClaimType),
Issuer = issuer ?? JwtBearerSettings.Issuers?.FirstOrDefault(),
Audience = audience ?? JwtBearerSettings.Audiences?.FirstOrDefault(),
IssuedAt = now,
NotBefore = now.Add(-jwtBearerSettings.ClockSkew),
Expires = absoluteExpiration ?? (jwtBearerSettings.ExpirationTime.GetValueOrDefault() > TimeSpan.Zero ? now.Add(jwtBearerSettings.ExpirationTime!.Value) : DateTime.MaxValue),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtBearerSettings.SecurityKey)), jwtBearerSettings.Algorithm)
NotBefore = now.Add(-JwtBearerSettings.ClockSkew),
Expires = absoluteExpiration ?? (JwtBearerSettings.ExpirationTime.GetValueOrDefault() > TimeSpan.Zero ? now.Add(JwtBearerSettings.ExpirationTime!.Value) : DateTime.MaxValue),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtBearerSettings.SecurityKey)), JwtBearerSettings.Algorithm)
Comment on lines +20 to +37
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateTokenAsync does not enforce the interface contract that absoluteExpiration must be >= DateTime.UtcNow (see IJwtBearerService XML docs). If a past absoluteExpiration is provided, the method will mint an already-expired token rather than throwing ArgumentException as documented.

Copilot uses AI. Check for mistakes.
};

var tokenHandler = new JsonWebTokenHandler();
Expand All @@ -35,7 +43,8 @@ public Task<string> CreateTokenAsync(string userName, IList<Claim>? claims = nul
return Task.FromResult(token);
}

public async Task<ClaimsPrincipal> ValidateTokenAsync(string token, bool validateLifetime = true)
/// <inheritdoc />
public virtual async Task<ClaimsPrincipal> ValidateTokenAsync(string token, bool validateLifetime = true)
{
var tokenHandler = new JsonWebTokenHandler();

Expand All @@ -46,23 +55,23 @@ public async Task<ClaimsPrincipal> ValidateTokenAsync(string token, bool validat

var tokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = jwtBearerSettings.SchemeName,
NameClaimType = jwtBearerSettings.NameClaimType,
RoleClaimType = jwtBearerSettings.RoleClaimType,
ValidateIssuer = jwtBearerSettings.Issuers?.Any() ?? false,
ValidIssuers = jwtBearerSettings.Issuers,
ValidateAudience = jwtBearerSettings.Audiences?.Any() ?? false,
ValidAudiences = jwtBearerSettings.Audiences,
AuthenticationType = JwtBearerSettings.SchemeName,
NameClaimType = JwtBearerSettings.NameClaimType,
RoleClaimType = JwtBearerSettings.RoleClaimType,
ValidateIssuer = JwtBearerSettings.Issuers?.Any() ?? false,
ValidIssuers = JwtBearerSettings.Issuers,
ValidateAudience = JwtBearerSettings.Audiences?.Any() ?? false,
ValidAudiences = JwtBearerSettings.Audiences,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtBearerSettings.SecurityKey)),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtBearerSettings.SecurityKey)),
RequireExpirationTime = true,
ValidateLifetime = validateLifetime,
ClockSkew = jwtBearerSettings.ClockSkew
ClockSkew = JwtBearerSettings.ClockSkew
};

var validationResult = await tokenHandler.ValidateTokenAsync(token, tokenValidationParameters);

if (!validationResult.IsValid || validationResult.SecurityToken is not JsonWebToken jsonWebToken || jsonWebToken.Alg != jwtBearerSettings.Algorithm)
if (!validationResult.IsValid || validationResult.SecurityToken is not JsonWebToken jsonWebToken || jsonWebToken.Alg != JwtBearerSettings.Algorithm)
{
throw new SecurityTokenException("Token is expired or invalid", validationResult.Exception);
}
Expand All @@ -71,12 +80,13 @@ public async Task<ClaimsPrincipal> ValidateTokenAsync(string token, bool validat
return principal;
}

public async Task<string> RefreshTokenAsync(string token, bool validateLifetime, DateTime? absoluteExpiration = null)
/// <inheritdoc />
public virtual async Task<string> RefreshTokenAsync(string token, bool validateLifetime, DateTime? absoluteExpiration = null)
{
var principal = await ValidateTokenAsync(token, validateLifetime);
var claims = (principal.Identity as ClaimsIdentity)!.Claims.ToList();

var userName = claims.First(c => c.Type == jwtBearerSettings.NameClaimType).Value;
var userName = claims.First(c => c.Type == JwtBearerSettings.NameClaimType).Value;
var issuer = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Iss)?.Value;
var audience = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Aud)?.Value;

Expand Down
Loading