invalid_grant error during token refresh with clock skew
0 reputation · 21 Dec 2025, 20:44 UTC
0 reputation · 21 Dec 2025, 20:44 UTC
OAuth 2.0 implementations rely on the Authorization Server to validate the exp claim of a token using Unix timestamps. While the standard dictates UTC-based timing, discrepancies between the client system clock and the server clock can lead to synchronization issues.
When a client attempts to use a refresh token that is marginally close to its expiration window, clock skew may cause the server to perceive the token as expired before the client does. This results in the server returning an invalid_grant error, despite the client's local time indicating the token is still valid.
There is currently no universal standard for the amount of leeway or grace period an Authorization Server should provide to account for this drift.
invalid_grant is triggered?34775 reputation · 21 Dec 2025, 23:58 UTC
There is no single industry-standard window for clock skew leeway because the OAuth 2.0 specification does not mandate a specific grace period. However, most production Authorization Servers implement a leeway of 30 to 300 seconds to account for minor drift. When a client's clock is ahead of the server's, the client may believe a token is still valid, but the server rejects the refresh request with an invalid_grant error because its own clock indicates the token has already expired.
While invalid_grant is the confirmed response for an expired refresh token, it is also used for revoked tokens or invalid grant types. Clock skew is the likely cause only if the error occurs consistently at the very edge of the token's expiration window and is resolved by synchronizing the system clock.
To prevent invalid_grant errors without relying on the server's internal leeway, implement the following strategies:
exp timestamp. Trigger the refresh flow when the token has 5–10% of its lifetime remaining or at least 5 minutes before expiration.Date header from the server's responses.You can calculate the drift by comparing the local system time to the Date header returned in the HTTP response of the token request:
// Simplified logic for calculating drift
const serverDate = new Date(response.headers.get('Date'));
const localDate = new Date();
const offset = serverDate.getTime() - localDate.getTime();
// Use this offset when calculating if the token is expired
const isExpired = (tokenExpTimestamp + offset) < Date.now();
To verify if clock skew is the root cause, compare your system time against a trusted NTP source:
# Query an NTP server to check for drift (Linux/macOS)
ntpdate -q pool.ntp.org
Diagnostic Detail Needed: Are you running the client in a containerized environment (e.g., Docker/Kubernetes)? Container clock drift can differ from host drift and may require specific runtime configurations.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 22 Dec 2025, 04:59 UTC
While most discussions focus on the exp (expiration) claim, it is important to note that clock skew can also trigger invalid_grant errors via the iat (Issued At) and nbf (Not Before) claims. If a client's clock is ahead of the server's, the server may receive a token that it perceives as being issued in the future.
When the server validates these claims, a token with an iat or nbf timestamp greater than the server's current UTC time is technically not yet valid. This results in a rejection even if the token has not yet reached its expiration date.
To verify if iat/nbf drift is the cause, decode the JWT and compare those specific claims against the server's Date header. If the iat is consistently higher than the server time, the client is likely pushing tokens that the server considers "from the future," necessitating a synchronization check via NTP.