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/JwtBearer/JwtBearerService.cs b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs index ba94857..53dac5f 100644 --- a/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs +++ b/src/SimpleAuthentication/JwtBearer/JwtBearerService.cs @@ -6,27 +6,40 @@ 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 JwtBearerSettings JwtBearerSettings { get; } = jwtBearerSettingsOptions?.Value ?? throw new ArgumentNullException(nameof(jwtBearerSettingsOptions)); - 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) { + 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(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(); @@ -35,7 +48,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(); @@ -46,23 +60,23 @@ public async Task 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); } @@ -71,12 +85,13 @@ 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(); - 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; 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 @@ - + 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 }; });