Error message
IDX10223: Lifetime validation failed. The token is expired.
Also printed as
- Bearer error="invalid_token", error_description="The token expired at '10/20/2026 06:28:30'"
- IDX10222: Lifetime validation failed. The token is not yet valid.
Text after "The token is expired." contains both timestamps in the server's culture format. The ASP.NET Core `WWW-Authenticate` description is produced by `JwtBearerHandler`.
Microsoft.IdentityModel.Tokens (.NET) — SecurityTokenExpiredException
What it means
The token's `exp` is earlier than the current time minus `ClockSkew` (five minutes by default). The signature and other claims may be valid; the token is simply past its lifetime. The message goes on with "ValidTo (UTC): …, Current time (UTC): …", and ASP.NET Core reports `error_description="The token expired at '…'"` to the client. IDX10222 is the opposite case: `nbf` lies in the future.
Why it happens
The token's lifetime is over
commonEntra ID access tokens live 60–90 minutes by default; other providers often 5–15 minutes. A client that keeps using the same token past `exp` fails exactly on time.
Clock skew beyond the tolerance
occasionalWith a reduced `ClockSkew` (for example `TimeSpan.Zero`, a common copy-paste), even seconds of drift between the issuer and the API reject fresh tokens.
A token cached outside the token library
occasionalTokens stored in a database, a static field or a configuration file are never refreshed. MSAL and similar libraries refresh automatically if you let them.
How to fix it
- 1.
Keep a small clock skew
Tolerate drift of seconds, not minutes, and fix the clocks. Never set `ValidateLifetime = false` in production.
csharp options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "https://login.example.com/", ValidAudience = "api://orders", ClockSkew = TimeSpan.FromSeconds(30), // default is 5 minutes; lower it, do not raise it to hide expiry }; - 2.
Acquire tokens through the library on every call
`AcquireTokenSilent` returns a valid token from cache or refreshes it, so expiry is handled before the request.
csharp // Client side (MSAL): ask for a token every time; MSAL returns the cached one or refreshes it. var result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(); httpClient.DefaultRequestHeaders.Authorization = new("Bearer", result.AccessToken);