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, our DateTime.UtcNow can be 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.