{"record":{"id":"82b9a466d4e6341f","repo":"BookStackApp/BookStack","slug":"failed-to-read-signing-key-with-error","errorCode":null,"errorMessage":"Failed to read signing key with error: ","messagePattern":"Failed to read signing key with error: ","errorType":"exception","errorClass":"OidcInvalidTokenException","httpStatus":null,"severity":"error","filePath":"app/Access/Oidc/OidcJwtWithClaims.php","lineNumber":130,"sourceCode":"        }\n    }\n\n    /**\n     * Validate the signature of the given token and ensure it validates against the provided key.\n     *\n     * @throws OidcInvalidTokenException\n     */\n    protected function validateTokenSignature(): void\n    {\n        if ($this->header['alg'] !== 'RS256') {\n            throw new OidcInvalidTokenException(\"Only RS256 signature validation is supported. Token reports using {$this->header['alg']}\");\n        }\n\n        $parsedKeys = array_map(function ($key) {\n            try {\n                return new OidcJwtSigningKey($key);\n            } catch (OidcInvalidKeyException $e) {\n                throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());\n            }\n        }, $this->keys);\n\n        $contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];\n        /** @var OidcJwtSigningKey $parsedKey */\n        foreach ($parsedKeys as $parsedKey) {\n            if ($parsedKey->verify($contentToSign, $this->signature)) {\n                return;\n            }\n        }\n\n        throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');\n    }\n\n    /**\n     * Validate common claims for OIDC JWT tokens.\n     * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation\n     * and https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Access/Oidc/OidcJwtWithClaims.php#L112-L148","documentation":"This library validates an OIDC/JWT ID token by parsing each configured signing key into an OidcJwtSigningKey object. If a key string is malformed (not valid PEM/JWK key material), OidcJwtSigningKey's constructor throws OidcInvalidKeyException, which is wrapped and rethrown here as OidcInvalidTokenException with the underlying reason appended to the message.","triggerScenarios":"OidcJwtWithClaims::validateTokenSignature() (called via validateCommonTokenDetails) maps every key in $this->keys through 'new OidcJwtSigningKey($key)'; any key that fails construction throws. Typical causes: truncated PEM (missing header/footer lines), keys stored with escaped or mangled newlines (e.g. env var without \\n preserved), a non-key value (HTML, placeholder text) fetched from the JWKS/issuer metadata endpoint, or a key in an unsupported format (DER, SSH format, certificate where a bare public key is expected).","commonSituations":"OIDC_ID_TOKEN_PUBLIC_KEY / signing-key env var copied from a terminal losing newlines; pasting a private key where a public key is required; a reverse proxy or error page returned where the JWKS was expected; base64-encoded key not decoded before being passed to the library; rotation where the new key was committed corrupted.","solutions":["Inspect the message suffix for the underlying OidcInvalidKeyException reason and verify each configured key is complete, valid PEM (-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----) with real newlines, not literal '\\n'","If the key comes from an env var or single-line config, convert it to proper multiline form (e.g. use a secrets file, or replace literal \\n with real newlines)","Confirm the key is fetched from the correct discovery/JWKS URL and that the response is key material, not an HTML error page","Verify you are supplying the right key type for the token's alg (e.g. RS256 RSA public key, not EC/Ed25519 or a certificate) and that Base64/dot-separated JWK components are decoded correctly","Regenerate or re-export the key if it is truncated or corrupt: openssl pkey -pubin -in key.pem -text -noout should parse it"],"exampleFix":"// before: single-line env value with literal \\n\n$keys = [getenv('OIDC_PUBLIC_KEY')]; // \"-----BEGIN PUBLIC KEY-----\\nMIIBI...\\n-----END PUBLIC KEY-----\" as text\n// after: restore real newlines before constructing the validator\n$raw = getenv('OIDC_PUBLIC_KEY');\n$pem = str_replace('\\\\n', \"\\n\", $raw);\n$keys = [$pem];","handlingStrategy":"validation","validationCode":"function isValidPem(string $key): bool {\n    $t = str_replace('\\\\n', \"\\n\", trim($key));\n    return str_starts_with($t, '-----BEGIN')\n        && str_contains($t, '-----END')\n        && openssl_pkey_get_public($t) !== false;\n}\n$keys = array_values(array_filter($keys, 'isValidPem'));","typeGuard":null,"tryCatchPattern":"try {\n    $jwt->validateCommonTokenDetails($token, $clientId);\n} catch (OidcInvalidTokenException $e) {\n    if (str_starts_with($e->getMessage(), 'Failed to read signing key')) {\n        $this->logger->error('OIDC signing key unreadable — check key config', ['detail' => $e->getMessage()]);\n        throw new ConfigurationException('Invalid OIDC signing key configured');\n    }\n    throw $e;\n}","preventionTips":["Store signing keys as files or secret-manager entries with preserved newlines, not inline env strings","Pre-parse keys with openssl_pkey_get_public()/phpseclib loaders at startup to fail fast on bad config","Never commit truncated keys; verify with openssl pkey -pubin after any rotation","Use the same key-loading helper everywhere so PEM normalization (\\n restoration, header/footer) happens once"],"tags":["php","oidc","jwt","signing-key","configuration"],"backgroundTag":"invalid-jwt-signing-key","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}