Thursday, May 28, 2026

A lesson learned about JWT tokens "Issued At" attribute

One of our systems integrates with Apple Pay and a JWT token is used to authenticate server-to-server requests from us to their backends. Someone noticed that a small amount of requests fail. It was usually less than 5% of failed requests, raising to 30% occasionally for short period of times.

The code was audited and we've found that someone just wrote:

private string GetToken()
{
	var now    = DateTime.UtcNow;
	var expiry = now.AddSeconds(this.Settings.MaxTokenAge);

	ECDsaSecurityKey eCDsaSecurityKey = GetEcdsaSecuritKey();

	var handler = new JsonWebTokenHandler();
	string jwt = handler.CreateToken(new SecurityTokenDescriptor
	{
		Issuer   = this.Settings.IssuerId,
		Audience = this.Settings.AppstoreAudience,
        
		NotBefore = now,
		Expires   = expiry,
		IssuedAt  = now,
		Claims    = new Dictionary<string, object>
		{
        	...
		},

		SigningCredentials = ...
	});

	return jwt;
}

Looks great.

Problem is, it does not always work.

What we've found out is that when there's a subtle, small difference of current time between your servers and their servers, your DateTime.UtcNow is their future. And they (correctly) reject tokens from the future.

What was applied there? Well, just:

private string GetToken()
{
	var now    = DateTime.UtcNow.AddMinutes(-1);
	var expiry = now.AddSeconds(this.Settings.MaxTokenAge);

The result? 0% of failed requests.

Lesson learned.