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 registered

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. 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
  2. Decode the token header (first base64url segment) to read its kid and alg, and confirm you configured the matching key for that kid
  3. Verify the token was not truncated/altered in transit — compare the raw token string end-to-end from issuer to your app
  4. Check for multi-tenant/key mismatches: the issuer that minted the token must be the issuer whose key you configured
  5. 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

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


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