hiddenhenry
New member
- Joined
- Apr 30, 2025
- Messages
- 3
- Programming Experience
- 1-3
My application is a .NET 9 web API. I configure authentication with this extension method
```cs
public static void SetupAuthentication(this IServiceCollection services)
{
var authSettings = services.BuildServiceProvider().GetService<IOptionsSnapshot<AuthSettings>>()?.Value;
// JWT Configuration
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = authSettings!.Jwt.Issuer,
ValidAudience = authSettings!.Jwt.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings!.Jwt.Secret)),
ClockSkew = TimeSpan.Zero,
};
})
.AddGoogle(options =>
{
options.ClientId = authSettings!.Google.ClientId;
options.ClientSecret = authSettings!.Google.ClientSecret;
});
}
```
Elsewhere, I register TimeProvider with
```cs
public static void RegisterServices(this IServiceCollection services)
{
services.AddScoped<ITokenService, TokenService>();
// used for time manipulation and testing
// we should use this instead of DateTime.Now
services.TryAddSingleton(TimeProvider.System);
}
```
I use a method in `TokenService` to create JWTs that can be used to access the API. This method usually looks like this:
```cs
public AccessTokenResponse CreateAccessToken(ApplicationUser user, string[] roles)
{
List<Claim> claims =
[
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.EmailVerified, user.EmailConfirmed.ToString()),
];
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512Signature);
var expiresAt = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryInMinutes);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiresAt,
SigningCredentials = credentials,
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AccessTokenResponse(tokenHandler.WriteToken(token), expiresAt);
}
```
But now I wish to use Timeprovider so I can test token expiration plus some. So I updated the method, now it looks like this:
```cs
public AccessTokenResponse CreateAccessToken(ApplicationUser user, string[] roles)
{
List<Claim> claims =
[
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.EmailVerified, user.EmailConfirmed.ToString()),
];
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512Signature);
var now = timeProvider.GetUtcNow();
var expiresAt = now.DateTime.AddMinutes(_jwtSettings.ExpiryInMinutes);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiresAt,
SigningCredentials = credentials,
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
NotBefore = now.DateTime,
IssuedAt = now.DateTime,
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AccessTokenResponse(tokenHandler.WriteToken(token), expiresAt);
}
```
Of course, `timeProvider` is injected into the service for the DI container to resolve. Now, when I run my tests, I get this error:
```text
[Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler] - Bearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo (UTC): '12/31/1999 11:15:00 PM', Current time (UTC): '4/30/2025 3:06:23 PM'.
OR
System.ArgumentException: IDX12401: Expires: '01/01/2000 00:15:00' must be after NotBefore: '30/04/2025 08:08:05'.
at System.IdentityModel.Tokens.Jwt.JwtPayload.AddFirstPriorityClaims(String issuer, String audience, IList`1 audiences, Nullable`1 notBefore, Nullable`1 expires, Nullable`1 issuedAt)
```
I've tried setting TimeProvider in my authentication configuration - no luck. I've tried setting a value for NowBefore and IssuedAt in `SecurityTokenDescriptor` - no luck.
For reference, here's the CustomWebApplicationFactory where I register a FakeTimeProvider:
```cs
public class CustomWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
public readonly FakeTimeProvider FakeTimeProvider = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
var timeProviderDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TimeProvider));
if (timeProviderDescriptor != null)
{
services.Remove(timeProviderDescriptor);
services.AddSingleton<TimeProvider>(FakeTimeProvider);
}
});
}
}
```
I've also tried initialising FakeTimeProvider with a specific date - no luck. Am I doing something wrong? Is there a specific way to setup TimeProvider in integration tests?
My aim is to use it everywhere, instead of the static DateTime methods.
```cs
public static void SetupAuthentication(this IServiceCollection services)
{
var authSettings = services.BuildServiceProvider().GetService<IOptionsSnapshot<AuthSettings>>()?.Value;
// JWT Configuration
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = authSettings!.Jwt.Issuer,
ValidAudience = authSettings!.Jwt.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings!.Jwt.Secret)),
ClockSkew = TimeSpan.Zero,
};
})
.AddGoogle(options =>
{
options.ClientId = authSettings!.Google.ClientId;
options.ClientSecret = authSettings!.Google.ClientSecret;
});
}
```
Elsewhere, I register TimeProvider with
```cs
public static void RegisterServices(this IServiceCollection services)
{
services.AddScoped<ITokenService, TokenService>();
// used for time manipulation and testing
// we should use this instead of DateTime.Now
services.TryAddSingleton(TimeProvider.System);
}
```
I use a method in `TokenService` to create JWTs that can be used to access the API. This method usually looks like this:
```cs
public AccessTokenResponse CreateAccessToken(ApplicationUser user, string[] roles)
{
List<Claim> claims =
[
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.EmailVerified, user.EmailConfirmed.ToString()),
];
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512Signature);
var expiresAt = DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryInMinutes);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiresAt,
SigningCredentials = credentials,
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AccessTokenResponse(tokenHandler.WriteToken(token), expiresAt);
}
```
But now I wish to use Timeprovider so I can test token expiration plus some. So I updated the method, now it looks like this:
```cs
public AccessTokenResponse CreateAccessToken(ApplicationUser user, string[] roles)
{
List<Claim> claims =
[
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.EmailVerified, user.EmailConfirmed.ToString()),
];
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret));
var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512Signature);
var now = timeProvider.GetUtcNow();
var expiresAt = now.DateTime.AddMinutes(_jwtSettings.ExpiryInMinutes);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiresAt,
SigningCredentials = credentials,
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
NotBefore = now.DateTime,
IssuedAt = now.DateTime,
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AccessTokenResponse(tokenHandler.WriteToken(token), expiresAt);
}
```
Of course, `timeProvider` is injected into the service for the DI container to resolve. Now, when I run my tests, I get this error:
```text
[Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler] - Bearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo (UTC): '12/31/1999 11:15:00 PM', Current time (UTC): '4/30/2025 3:06:23 PM'.
OR
System.ArgumentException: IDX12401: Expires: '01/01/2000 00:15:00' must be after NotBefore: '30/04/2025 08:08:05'.
at System.IdentityModel.Tokens.Jwt.JwtPayload.AddFirstPriorityClaims(String issuer, String audience, IList`1 audiences, Nullable`1 notBefore, Nullable`1 expires, Nullable`1 issuedAt)
```
I've tried setting TimeProvider in my authentication configuration - no luck. I've tried setting a value for NowBefore and IssuedAt in `SecurityTokenDescriptor` - no luck.
For reference, here's the CustomWebApplicationFactory where I register a FakeTimeProvider:
```cs
public class CustomWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
public readonly FakeTimeProvider FakeTimeProvider = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
var timeProviderDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TimeProvider));
if (timeProviderDescriptor != null)
{
services.Remove(timeProviderDescriptor);
services.AddSingleton<TimeProvider>(FakeTimeProvider);
}
});
}
}
```
I've also tried initialising FakeTimeProvider with a specific date - no luck. Am I doing something wrong? Is there a specific way to setup TimeProvider in integration tests?
My aim is to use it everywhere, instead of the static DateTime methods.