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.
 
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.

This is untested and theoretical but let's break down the problem and a possible solution.

The core issue is that your token validation logic (which uses Microsoft.IdentityModel.Tokens) is using the system's current time (derived from DateTime.UtcNow by default) to check the token's expiration (exp claim) and not-before (nbf claim).

You are correctly using FakeTimeProvider to generate the token's exp, nbf, and iat claims based on your controlled time. However, the validation process doesn't know about your FakeTimeProvider unless you explicitly tell it to use it.

The TokenValidationParameters class has a TimeProvider property specifically for this purpose. By default, it's null, which makes it fall back to TimeProvider.System (or effectively DateTime.UtcNow).

You need to configure your TokenValidationParameters in your test environment to use your FakeTimeProvider instance.

Here's the standard way to achieve this using the options pattern and IPostConfigureOptions:

1. Create a Post-Configuration Class

Create a class that implements IPostConfigureOptions<JwtBearerOptions>. This class will resolve the TimeProvider from the service provider after the initial JwtBearerOptions have been configured and set it on the TokenValidationParameters.

C#

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using System;

public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
private readonly TimeProvider _timeProvider;

public ConfigureJwtBearerOptions(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}

public void PostConfigure(string? name, JwtBearerOptions options)
{
// Apply this configuration to the default scheme or a specific named scheme
// if you have multiple. For typical setups, name will be null for the default.
if (name == null || name == JwtBearerDefaults.AuthenticationScheme)
{
options.TokenValidationParameters.TimeProvider = _timeProvider;
}
}
}

2. Register the Post-Configuration Class

You need to register this class in your application's service collection. This registration should happen wherever your AddAuthentication is called, or within a related service registration method.

In your RegisterServices method (or a similar startup method):

C#

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System;

public static class ServiceCollectionExtensions
{
public static void RegisterServices(this IServiceCollection services)
{
// ... other registrations

services.AddScoped<ITokenService, TokenService>();

// Register the default TimeProvider.System for the main application
services.TryAddSingleton(TimeProvider.System);

// Register the post-configuration class to set TokenValidationParameters.TimeProvider
// This will pick up the TimeProvider that is ultimately registered in the container
// (which will be FakeTimeProvider in your test factory)
services.AddSingleton<IPostConfigureOptions<JwtBearerOptions>, ConfigureJwtBearerOptions>();

// ... other registrations
}
}

And ensure your SetupAuthentication is called before or alongside RegisterServices if they are separate methods, or just ensure RegisterServices is called as part of your application startup sequence. The order of calling AddAuthentication and registering the post-configure class doesn't strictly matter, as the post-configure runs later in the options pipeline.

Your SetupAuthentication method remains largely the same, you don't need to try and inject TimeProvider there directly:

C#

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Text;

public static class AuthenticationExtensions
{
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, // Keep this true!
ValidateIssuerSigningKey = true,
ValidIssuer = authSettings!.Jwt.Issuer,
ValidAudience = authSettings!.Jwt.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings!.Jwt.Secret)),
ClockSkew = TimeSpan.Zero, // Keep this Zero if you want strict validation
// DO NOT set options.TokenValidationParameters.TimeProvider here.
// It will be set by the IPostConfigureOptions later.
};
})
.AddGoogle(options =>
{
options.ClientId = authSettings!.Google.ClientId;
options.ClientSecret = authSettings!.Google.ClientSecret;
});
}
}

3. Your CustomWebApplicationFactory

Your factory setup is already correct. By adding services.AddSingleton<TimeProvider>(FakeTimeProvider); in ConfigureTestServices, you are replacing the default TimeProvider.System registration with your FakeTimeProvider specifically for the test server's service provider.

Since the IPostConfigureOptions<JwtBearerOptions> you registered resolves TimeProvider from the service provider when configuring the options, it will pick up your FakeTimeProvider instance.

C#

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

public class CustomWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
public readonly FakeTimeProvider FakeTimeProvider = new FakeTimeProvider(); // Make sure FakeTimeProvider is initialized

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Remove the application's default TimeProvider registration
var timeProviderDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TimeProvider));
if (timeProviderDescriptor != null)
{
services.Remove(timeProviderDescriptor);
}

// Add the FakeTimeProvider for the test environment
services.AddSingleton<TimeProvider>(FakeTimeProvider);

// The IPostConfigureOptions<JwtBearerOptions> registered in your
// application's service collection will now pick up this FakeTimeProvider
// when configuring the JwtBearerOptions for the test server.
});
}

// Implement IAsyncLifetime methods if needed for setup/teardown
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
}

4. Your TokenService

Your updated TokenService method is correct as it uses the injected timeProvider to determine the NotBefore, IssuedAt, and Expires times for the token. This ensures the token's lifetime claims are based on the same controlled time source.

C#

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

// Assuming AccessTokenResponse and ApplicationUser are defined elsewhere
public class AccessTokenResponse
{
public string Token { get; }
public DateTime ExpiresAt { get; }

public AccessTokenResponse(string token, DateTime expiresAt)
{
Token = token;
ExpiresAt = expiresAt;
}
}

// Assuming AuthSettings and JwtSettings are defined elsewhere
public class AuthSettings { public JwtSettings Jwt { get; set; } = new(); /* ... other settings */ }
public class JwtSettings { public string Secret { get; set; } = string.Empty; public string Issuer { get; set; } = string.Empty; public string Audience { get; set; } = string.Empty; public int ExpiryInMinutes { get; set; } }


public interface ITokenService
{
AccessTokenResponse CreateAccessToken(ApplicationUser user, string[] roles);
}


public class TokenService : ITokenService
{
private readonly JwtSettings _jwtSettings;
private readonly TimeProvider timeProvider; // Injected

public TokenService(IOptionsSnapshot<AuthSettings> authSettings, TimeProvider timeProvider)
{
_jwtSettings = authSettings.Value.Jwt;
this.timeProvider = timeProvider;
}

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(); // Use the injected TimeProvider
var expiresAt = now.DateTime.AddMinutes(_jwtSettings.ExpiryInMinutes);

// Ensure expiresAt is always after now.DateTime if ExpiryInMinutes > 0
if (_jwtSettings.ExpiryInMinutes > 0 && expiresAt <= now.DateTime)
{
// This case should ideally not happen if calculations are correct
// but adding a small buffer or logging might be useful in complex scenarios.
// For standard use, expiresAt = now.DateTime.AddMinutes(_jwtSettings.ExpiryInMinutes); is fine.
}


var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiresAt,
SigningCredentials = credentials,
Issuer = _jwtSettings.Issuer,
Audience = _jwtSettings.Audience,
NotBefore = now.DateTime, // Also use the injected TimeProvider
IssuedAt = now.DateTime, // Also use the injected TimeProvider
};

var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AccessTokenResponse(tokenHandler.WriteToken(token), expiresAt);
}
}

Summary Solution:

  1. Register TimeProvider.System (via TryAddSingleton) in your main application's service configuration for production behavior.
  2. Register an IPostConfigureOptions<JwtBearerOptions> implementation (ConfigureJwtBearerOptions) in your application's service configuration. This class will resolve TimeProvider and set options.TokenValidationParameters.TimeProvider.
  3. In your CustomWebApplicationFactory, remove the application's TimeProvider registration and add your FakeTimeProvider as a Singleton within the test service collection.
  4. Ensure your TokenService injects and uses TimeProvider for calculating token lifetime claims (nbf, iat, exp).
By following these steps, the JWT validation process within your test server will use the FakeTimeProvider instance provided by your factory, ensuring that the validation checks are performed against your controlled time, not the system's clock. This will resolve the IDX10223 and potentially IDX12401 errors because the token creation time and validation time will be synchronized via the FakeTimeProvider.
 
@Justin : Please put your code in code blocks.
 
Back
Top Bottom