How to setup JWT authentication with TimeProvider in .NET 9 integration tests - token validation fails with expiration errors

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.
 
Unfortunately, this site uses BBCode, not MarkDown. Please put your code in CODE tags.
 
Anyway, just a guess, but is it possible that if IssuedAt and NotBefore are set, you must also set Expires?

Also does the JWT standard allow IssuedAt and NotBefore to be the exact same value? I would hope so...
 
The same post, but with BB code. I couldn't find an edit button, so this will have to do.

C#:
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
C#:
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:

C#:
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:

C#:
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:

C#:
[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:

C#:
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);
}
});
}
}
 
Last edited by a moderator:
Anyway, just a guess, but is it possible that if IssuedAt and NotBefore are set, you must also set Expires?

Also does the JWT standard allow IssuedAt and NotBefore to be the exact same value? I would hope so...

I've always set a value for Expires. Until I tried using TimeProvider, there hadn't been need to set NotBefore and IssuedAt manually as the framework would take care of it. I figured that the time 'source' differed somehow, so I did some Googling and found that I can set a timeProvider value when configuring JwtBearer, so I did.

C#:
public static void SetupAuthentication(this IServiceCollection services)
    {
        var serviceProvider = services.BuildServiceProvider(); 
        var authSettings = serviceProvider.GetService<IOptionsSnapshot<AuthSettings>>()?.Value;
        var timeProvider = serviceProvider.GetService<TimeProvider>();

        // 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,
                };
                options.TimeProvider = timeProvider;
            })
            .AddGoogle(options =>
            {
                options.ClientId = authSettings!.Google.ClientId;
                options.ClientSecret = authSettings!.Google.ClientSecret;
                options.TimeProvider = timeProvider;
            });
    }

But this had no effect either, and I've been unable to find any documentation on it all day.
 
I've been unable to find any documentation on it all day.

Yeah, me too. It's why in my post #3, I mentioned that I was just guessing.

Any luck stepping into the library code to see what is happening?
 
Hmm... If you set the expiration time, why is it a value that is in the past according to the error message.
 
Back
Top Bottom