From 1203a9ac61bf4f9bcea2d8e1875394b6452ed480 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:50:21 +0000 Subject: [PATCH 1/6] Initial plan From a6e42b8b91fc14debdc9daafbfe7bd99cf208e03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:54:24 +0000 Subject: [PATCH 2/6] Make JwtBearerService public with virtual methods for extensibility Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- .../JwtBearer/JwtBearerService.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs index ba94857..302c982 100644 --- a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs +++ b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs @@ -6,11 +6,19 @@ namespace SimpleAuthentication.JwtBearer; -internal class JwtBearerService(IOptions jwtBearerSettingsOptions) : IJwtBearerService +/// +/// Default implementation of that provides JWT Bearer token generation and validation. +/// +/// The JWT Bearer settings. +public class JwtBearerService(IOptions jwtBearerSettingsOptions) : IJwtBearerService { - private readonly JwtBearerSettings jwtBearerSettings = jwtBearerSettingsOptions.Value; + /// + /// Gets the JWT Bearer settings used by this service. + /// + protected readonly JwtBearerSettings jwtBearerSettings = jwtBearerSettingsOptions.Value; - public Task CreateTokenAsync(string userName, IList? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null) + /// + public virtual Task CreateTokenAsync(string userName, IList? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null) { claims ??= []; claims.Update(jwtBearerSettings.NameClaimType, userName); @@ -35,7 +43,8 @@ public Task CreateTokenAsync(string userName, IList? claims = nul return Task.FromResult(token); } - public async Task ValidateTokenAsync(string token, bool validateLifetime = true) + /// + public virtual async Task ValidateTokenAsync(string token, bool validateLifetime = true) { var tokenHandler = new JsonWebTokenHandler(); @@ -71,7 +80,8 @@ public async Task ValidateTokenAsync(string token, bool validat return principal; } - public async Task RefreshTokenAsync(string token, bool validateLifetime, DateTime? absoluteExpiration = null) + /// + public virtual async Task RefreshTokenAsync(string token, bool validateLifetime, DateTime? absoluteExpiration = null) { var principal = await ValidateTokenAsync(token, validateLifetime); var claims = (principal.Identity as ClaimsIdentity)!.Claims.ToList(); From 11332d39645ce50a5ef35c90a3296feee3cfa1c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:53:58 +0000 Subject: [PATCH 3/6] Change jwtBearerSettings from protected field to protected read-only property Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com> --- .../JwtBearer/JwtBearerService.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs index 302c982..ceeffca 100644 --- a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs +++ b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs @@ -15,26 +15,26 @@ public class JwtBearerService(IOptions jwtBearerSettingsOptio /// /// Gets the JWT Bearer settings used by this service. /// - protected readonly JwtBearerSettings jwtBearerSettings = jwtBearerSettingsOptions.Value; + protected JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions.Value; /// public virtual Task CreateTokenAsync(string userName, IList? 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) }; var tokenHandler = new JsonWebTokenHandler(); @@ -55,23 +55,23 @@ public virtual async Task ValidateTokenAsync(string token, bool 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); } @@ -86,7 +86,7 @@ public virtual async Task RefreshTokenAsync(string token, bool validateL 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; From cac6f2e1b76c77e9e2e43d9fa4deeef188e5c871 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 9 Apr 2026 11:31:19 +0200 Subject: [PATCH 4/6] Ensure JwtBearer settings are validated and lifetime checked - Throw ArgumentNullException if JwtBearerSettings options are null in JwtBearerService constructor. - Always enable token lifetime validation in JWT Bearer authentication, regardless of ExpirationTime setting. --- src/SimpleAuthentication/JwtBearer/JwtBearerService.cs | 2 +- src/SimpleAuthentication/SimpleAuthenticationExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs index ceeffca..94be671 100644 --- a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs +++ b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs @@ -15,7 +15,7 @@ public class JwtBearerService(IOptions jwtBearerSettingsOptio /// /// Gets the JWT Bearer settings used by this service. /// - protected JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions.Value; + protected JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions?.Value ?? throw new ArgumentNullException(nameof(jwtBearerSettingsOptions)); /// public virtual Task CreateTokenAsync(string userName, IList? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null) diff --git a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs index e5d76d5..8ac0155 100644 --- a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs +++ b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs @@ -112,7 +112,7 @@ static void CheckAddJwtBearer(AuthenticationBuilder builder, IConfigurationSecti ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecurityKey)), RequireExpirationTime = true, - ValidateLifetime = settings.ExpirationTime.GetValueOrDefault() > TimeSpan.Zero, + ValidateLifetime = true, ClockSkew = settings.ClockSkew }; }); From 00680ecdddba1f855e729b03941f856b5a4b7d3c Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 9 Apr 2026 11:40:04 +0200 Subject: [PATCH 5/6] Update Swashbuckle & ASP.NET Core package versions Bump Swashbuckle.AspNetCore.SwaggerUI and SwaggerGen from 10.1.5 to 10.1.7 across all projects. Update Swashbuckle.AspNetCore in Net8JwtBearerSample to 10.1.7. Also update Microsoft.AspNetCore.Authentication.JwtBearer and Microsoft.AspNetCore.OpenApi for .NET 10.0 to their latest patch versions. No other changes included. --- samples/Controllers/ApiKeySample/ApiKeySample.csproj | 4 ++-- .../BasicAuthenticationSample.csproj | 4 ++-- samples/Controllers/JwtBearerSample/JwtBearerSample.csproj | 4 ++-- samples/MinimalApis/ApiKeySample/ApiKeySample.csproj | 4 ++-- .../BasicAuthenticationSample.csproj | 4 ++-- samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj | 4 ++-- .../Net8JwtBearerSample/Net8JwtBearerSample.csproj | 2 +- .../SimpleAuthentication.Abstractions.csproj | 2 +- .../SimpleAuthentication.Swashbuckle.csproj | 2 +- src/SimpleAuthentication/SimpleAuthentication.csproj | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/samples/Controllers/ApiKeySample/ApiKeySample.csproj b/samples/Controllers/ApiKeySample/ApiKeySample.csproj index 4f40513..3052b54 100644 --- a/samples/Controllers/ApiKeySample/ApiKeySample.csproj +++ b/samples/Controllers/ApiKeySample/ApiKeySample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj index 4f40513..3052b54 100644 --- a/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj +++ b/samples/Controllers/BasicAuthenticationSample/BasicAuthenticationSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj b/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj index 4f40513..3052b54 100644 --- a/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/Controllers/JwtBearerSample/JwtBearerSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj index 84d35a1..8e34124 100644 --- a/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj +++ b/samples/MinimalApis/ApiKeySample/ApiKeySample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj index 84d35a1..8e34124 100644 --- a/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj +++ b/samples/MinimalApis/BasicAuthenticationSample/BasicAuthenticationSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj b/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj index 84d35a1..8e34124 100644 --- a/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj +++ b/samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj index 1c38ccb..17c6dda 100644 --- a/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj +++ b/samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj b/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj index 024ab07..ff7fe75 100644 --- a/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj +++ b/src/SimpleAuthentication.Abstractions/SimpleAuthentication.Abstractions.csproj @@ -36,7 +36,7 @@ - + diff --git a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj index 19252c8..a493341 100644 --- a/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj +++ b/src/SimpleAuthentication.Swashbuckle/SimpleAuthentication.Swashbuckle.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/SimpleAuthentication/SimpleAuthentication.csproj b/src/SimpleAuthentication/SimpleAuthentication.csproj index 7478133..bcf7d7b 100644 --- a/src/SimpleAuthentication/SimpleAuthentication.csproj +++ b/src/SimpleAuthentication/SimpleAuthentication.csproj @@ -35,7 +35,7 @@ - + From 711c23f3900405df934ace7e267f3ac49f13d976 Mon Sep 17 00:00:00 2001 From: Marco Minerva Date: Thu, 9 Apr 2026 11:49:53 +0200 Subject: [PATCH 6/6] Validate absoluteExpiration is not in the past for JWTs Add a check to ensure absoluteExpiration is not earlier than the current UTC time when creating a JWT token. Throw an ArgumentException if an invalid expiration is provided to prevent issuing already-expired tokens. --- src/SimpleAuthentication/JwtBearer/JwtBearerService.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs index 94be671..53dac5f 100644 --- a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs +++ b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs @@ -20,12 +20,17 @@ public class JwtBearerService(IOptions jwtBearerSettingsOptio /// public virtual Task CreateTokenAsync(string userName, IList? claims = null, string? issuer = null, string? audience = null, DateTime? absoluteExpiration = null) { + var now = DateTime.UtcNow; + + if (absoluteExpiration.HasValue && absoluteExpiration.Value < now) + { + throw new ArgumentException("The expiration date must be greater than or equal to the current date and time.", nameof(absoluteExpiration)); + } + claims ??= []; 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),