BookStackApp/BookStack · critical · OidcInvalidTokenException
Token signature could not be validated using the provided ke
Error message
Token signature could not be validated using the provided keys
What it means
The library parsed at least one signing key successfully, but none of the configured keys could verify the token's signature over header.payload. It throws OidcInvalidTokenException after every parsed key fails verify(). This means the token was not signed by any of the keys you supplied — a trust failure, not a formatting failure.
Source
Thrown at app/Access/Oidc/OidcJwtWithClaims.php:142
}
$parsedKeys = array_map(function ($key) {
try {
return new OidcJwtSigningKey($key);
} catch (OidcInvalidKeyException $e) {
throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());
}
}, $this->keys);
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
/** @var OidcJwtSigningKey $parsedKey */
foreach ($parsedKeys as $parsedKey) {
if ($parsedKey->verify($contentToSign, $this->signature)) {
return;
}
}
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
}
/**
* Validate common claims for OIDC JWT tokens.
* As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
* and https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
*
* @throws OidcInvalidTokenException
*/
protected function validateCommonClaims(string $clientId): void
{
// 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
// MUST exactly match the value of the iss (issuer) Claim.
if (empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']) {
throw new OidcInvalidTokenException('Missing or non-matching token issuer value');
}
// 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registeredView on GitHub (pinned to 18f8469a1c)
Solutions
- Refresh the signing key(s) from the issuer's JWKS endpoint (/.well-known/openid-configuration → jwks_uri) and ensure $this->keys contains the current key the IdP actually signs with
- Decode the token header (first base64url segment) to read its kid and alg, and confirm you configured the matching key for that kid
- Verify the token was not truncated/altered in transit — compare the raw token string end-to-end from issuer to your app
- Check for multi-tenant/key mismatches: the issuer that minted the token must be the issuer whose key you configured
- If tampering is plausible, treat it as a security event: reject the token (the library does) and investigate the token source
Example fix
// before: hardcoded stale key
$keys = [file_get_contents('/etc/keys/old-idp.pub')];
// after: fetch current keys from JWKS each rotation
$jwks = json_decode(file_get_contents($jwksUri), true);
$keys = array_map(fn($k) => "-----BEGIN PUBLIC KEY-----\n" . chunk_split($k['n'], 64, "\n") . "-----END PUBLIC KEY-----", $jwks['keys']); Defensive patterns
Strategy: try-catch
Validate before calling
$payload = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/') . '=='), true);
$header = json_decode(base64_decode(strtr(explode('.', $token)[0], '-_', '+/') . '=='), true);
// pre-check: token alg/kid must exist in your configured key set before validating
if (!in_array($header['alg'] ?? '', ['RS256', 'ES256'], true) || !isset($header['kid'])) {
throw new UnexpectedValueException('Token alg/kid not in configured key set');
} Try / catch
try {
$jwt->validateCommonTokenDetails($token, $clientId);
} catch (OidcInvalidTokenException $e) {
if ($e->getMessage() === 'Token signature could not be validated using the provided keys') {
$keys = $this->refreshKeysFromJwks($issuer); // key rotation recovery: refresh once and retry
try {
$jwt->validateCommonTokenDetails($token, $clientId);
} catch (OidcInvalidTokenException $e) {
throw new UnauthorizedException('ID token signature not trusted', 0, $e);
}
}
} Prevention
- Fetch keys dynamically from the issuer's jwks_uri and cache with a short TTL instead of hardcoding keys
- Log the token header kid (not the token) on failure to quickly identify key mismatches during rotation
- Pin-test with a known-valid token after every IdP key rotation
- Reject tokens that were transported through anything that could alter them (URL-encoding, HTML round-trips)
When it happens
Trigger: validateTokenSignature() iterates $parsedKeys and calls verify($contentToSign, $this->signature); if all return false, the exception is thrown. Happens when the IdP rotated keys and the configured key is stale, the token is signed with a different key than configured, or the token was tampered with/truncated after signing (signature bytes corrupted).
Common situations: Identity provider key rotation without updating the app's configured public key; configuring the IdP's private key or the wrong realm's public key; token forwarded through a proxy that altered it; copying a token and missing trailing characters; multi-tenant setups where a token from tenant A is validated with tenant B's key.
Related errors
- Could not parse out a valid {$prop} within the provided toke
- Could not parse out a valid signature within the provided to
- Only RS256 signature validation is supported. Token reports
- Failed to read signing key with error:
- Missing or non-matching token issuer value
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/23a1d3a2692f95e7.
Report an issue: GitHub.