BookStackApp/BookStack · error · OidcInvalidTokenException

Missing token expiration time value

Error message

Missing token expiration time value

What it means

An OIDC ID token must carry an exp (expiration) claim; validateTokenClaims rejects any token whose payload lacks an exp value. The spec requires exp so the client can enforce token lifetime, and BookStack hard-fails when it is missing rather than assuming a default.

Source

Thrown at app/Access/Oidc/OidcIdToken.php:54

        // Partially done in parent.
        $aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];
        if (count($aud) !== 1) {
            throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');
        }

        // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
        // NOTE: Addressed by enforcing a count of 1 above.

        // 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id
        // is the Claim Value.
        if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {
            throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id');
        }

        // 5. The current time MUST be before the time represented by the exp Claim
        // (possibly allowing for some small leeway to account for clock skew).
        if (empty($this->payload['exp'])) {
            throw new OidcInvalidTokenException('Missing token expiration time value');
        }

        $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) {

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Fix the IdP/token mapper so the ID token includes the standard exp claim
  2. Decode the token at jwt.io to confirm which claims the IdP actually emits
  3. If using a custom OIDC provider, ensure ID tokens comply with OIDC Core (iss, sub, aud, exp, iat required)
  4. Check whether a proxy or middleware is stripping claims from responses
  5. Update the IdP firmware/plugin version if a known bug omitted standard claims

Example fix

// before (custom token builder)
$payload = ['iss' => $iss, 'sub' => $sub, 'aud' => $aud];
// after
$payload = ['iss' => $iss, 'sub' => $sub, 'aud' => $aud,
            'exp' => time() + 300, 'iat' => time()];
Defensive patterns

Strategy: validation

Validate before calling

$payload = /* decode jwt payload */;
if (empty($payload['exp'])) {
    throw new InvalidArgumentException('ID token missing exp claim — fix IdP token mapper');
}

Type guard

function hasRequiredOidcClaims(?array $payload): bool {
    return is_array($payload)
        && isset($payload['iss'], $payload['sub'], $payload['aud'], $payload['exp'], $payload['iat']);
}

Try / catch

try {
    $idToken = OidcIdToken::validate($token, $clientId, $keys);
} catch (OidcInvalidTokenException $e) {
    if (str_contains($e->getMessage(), 'expiration')) {
        Log::error('OIDC token missing exp; IdP token configuration is non-compliant');
    }
    throw $e;
}

Prevention

When it happens

Trigger: validate() on an ID token whose decoded payload has no 'exp' key or an empty/falsy exp — produced by a custom/misconfigured IdP token mapper that omits the claim, or a hand-crafted/signing-test token.

Common situations: Custom Keycloak/Okta protocol mappers or a homegrown OIDC provider not including standard claims; stripped claims by a token-transforming proxy; tokens built manually in tests; using a non-standard 'id token' endpoint that returns minimal claims.

Related errors


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