BookStackApp/BookStack · error · OidcInvalidTokenException
Token issue at time is not recent or is invalid
Error message
Token issue at time is not recent or is invalid
What it means
After confirming 'iat' exists, validateTokenClaims checks the issued-at time is recent: it must not be newer than now + clock skew, and not older than 24 hours (time() - 86400). This catches replayed/stale tokens and clock drift between client and IdP. Tokens failing this window are rejected.
Source
Thrown at app/Access/Oidc/OidcIdToken.php:73
}
$skewSeconds = 120;
$now = time();
if ($now >= (intval($this->payload['exp']) + $skewSeconds)) {
throw new OidcInvalidTokenException('Token has expired');
}
// 6. The iat Claim can be used to reject tokens that were issued too far away from the current time,
// limiting the amount of time that nonces need to be stored to prevent attacks.
// The acceptable range is Client specific.
if (empty($this->payload['iat'])) {
throw new OidcInvalidTokenException('Missing token issued at time value');
}
$dayAgo = time() - 86400;
$iat = intval($this->payload['iat']);
if ($iat > ($now + $skewSeconds) || $iat < $dayAgo) {
throw new OidcInvalidTokenException('Token issue at time is not recent or is invalid');
}
// 7. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
// The meaning and processing of acr Claim Values is out of scope for this document.
// NOTE: Not used for our case here. acr is not requested.
// 8. When a max_age request is made, the Client SHOULD check the auth_time Claim value and request
// re-authentication if it determines too much time has elapsed since the last End-User authentication.
// NOTE: Not used for our case here. A max_age request is not made.
// Custom: Ensure the "sub" (Subject) Claim exists and has a value.
if (empty($this->payload['sub'])) {
throw new OidcInvalidTokenException('Missing token subject value');
}
}
}
View on GitHub (pinned to 18f8469a1c)
Solutions
- Sync the server clock (enable NTP, e.g. chrony/ntpdate) on both app and IdP hosts
- Obtain a fresh ID token instead of reusing a cached one older than 24 hours
- Verify the system timezone/date is correct (date -u) and hardware clock is not drifted
- If legitimate skew is expected, adjust $skewSeconds passed into the validation
Example fix
// before: relying on drifted host clock $token->validate($now); // after: keep host time correct sudo timedatectl set-ntp true && timedatectl status; // then re-run validation
Defensive patterns
Strategy: validation
Validate before calling
$iat = $payload['iat'] ?? null;
$skew = 300;
if (!is_numeric($iat) || $iat > time() + $skew || $iat < time() - 86400) { throw new \RuntimeException('iat out of acceptable window'); } Try / catch
try { $token->validate($now, $skewSeconds); } catch (OidcInvalidTokenException $e) { if (str_contains($e->getMessage(), 'not recent')) { /* refresh token / re-authenticate */ } throw $e; } Prevention
- Run NTP on app and IdP servers to eliminate clock skew
- Never cache or reuse ID tokens longer than the 24h validation window
- Request a fresh id_token on each login flow instead of replaying stored ones
- Monitor system clock drift and alert on large offsets
When it happens
Trigger: validate() on a token whose 'iat' is more than 86400 seconds in the past, or in the future beyond the allowed skew ($now + $skewSeconds).
Common situations: Server clock skew (VM clock drift, wrong timezone/NTP); IdP and app servers out of sync; cached/old tokens replayed; system clock set far in the future or past; long-lived stored tokens reused after a day.
Related errors
- Token has expired
- Token audience value has ' . count($aud) . ' values, Expecte
- Token authorized party exists but does not match the expecte
- Missing token expiration time value
- Missing token issued at time value
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/7f0fbbc21fb8e6b6.
Report an issue: GitHub.