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

  1. Sync the server clock (enable NTP, e.g. chrony/ntpdate) on both app and IdP hosts
  2. Obtain a fresh ID token instead of reusing a cached one older than 24 hours
  3. Verify the system timezone/date is correct (date -u) and hardware clock is not drifted
  4. 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

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


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/7f0fbbc21fb8e6b6. Report an issue: GitHub.