BookStackApp/BookStack · critical · OidcInvalidTokenException
Missing or non-matching token issuer value
Error message
Missing or non-matching token issuer value
What it means
Per the OIDC spec, the token's iss (issuer) claim must exactly match the issuer identifier this validator was configured with. When the payload has no iss claim, or the iss value differs from $this->issuer, validateCommonClaims throws OidcInvalidTokenException('Missing or non-matching token issuer value'). This prevents accepting tokens minted by a different authority.
Source
Thrown at app/Access/Oidc/OidcJwtWithClaims.php:157
}
}
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
// at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected
// if the ID Token does not list the Client as a valid audience.
if (empty($this->payload['aud'])) {
throw new OidcInvalidTokenException('Missing token audience value');
}
$aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];
if (!in_array($clientId, $aud, true)) {
throw new OidcInvalidTokenException('Token audience value did not match the expected client_id');
}
}
}
View on GitHub (pinned to 18f8469a1c)
Solutions
- Compare the exact iss claim in the token (base64url-decode the payload) with your configured issuer string — it must match byte-for-byte per the spec
- Fix trailing-slash or scheme mismatches: configure the issuer exactly as the IdP emits it in its discovery document (metadata.issuer)
- Confirm tokens come from the intended IdP/realm; if validating multiple issuers, use the per-issuer validator for each token
- If behind a proxy, ensure the issuer URL scheme/host matches what the IdP actually publishes, not a rewritten internal URL
Example fix
// before: trailing slash mismatch $issuer = 'https://idp.example.com/realms/main/'; // after: match the issuer exactly as it appears in discovery metadata $issuer = trim($issuer, '/'); // or better: copy metadata['issuer'] verbatim $validator = new OidcJwtWithClaims(/* ... */); $validator->setIssuer($issuer);
Defensive patterns
Strategy: validation
Validate before calling
$expectedIssuer = json_decode(
file_get_contents($issuer . '/.well-known/openid-configuration'), true
)['issuer'];
$payload = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/') . '=='), true);
if (($payload['iss'] ?? null) !== $expectedIssuer) {
throw new UnexpectedValueException('iss claim does not match configured issuer: ' . ($payload['iss'] ?? '<missing>'));
} Try / catch
try {
$jwt->validateCommonTokenDetails($token, $clientId);
} catch (OidcInvalidTokenException $e) {
if ($e->getMessage() === 'Missing or non-matching token issuer value') {
$claims = $this->decodePayload($token);
throw new UnauthorizedException(
'Issuer mismatch: expected ' . $expectedIssuer . ', got ' . ($claims['iss'] ?? 'none')
);
}
throw $e;
} Prevention
- Always source the configured issuer from the IdP's discovery metadata rather than hand-typing the URL
- Test both with and without trailing slashes in staging to catch config drift
- For multi-tenant setups, keep a map of tenant → issuer and validate against the tenant's issuer
- Never configure the issuer based on the incoming request host (proxy rewrites break exact matching)
When it happens
Trigger: validateCommonClaims($clientId), called from validateCommonTokenDetails, evaluates empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']. Triggers when: the token lacks an iss claim entirely; the configured issuer includes a trailing slash or scheme/host difference (http vs https, localhost vs 127.0.0.1, realm path mismatch) versus the iss claim; the token was issued by another issuer than the one configured.
Common situations: Issuer URL configured with trailing '/' while the IdP emits it without (or vice versa); environment-based issuer config (staging URL in production config); multi-tenant/multi-realm setup (Keycloak realms) where the realm in the URL doesn't match; scheme mismatch behind a TLS-terminating proxy (configured https, iss says http); case-sensitive path differences.
Related errors
- Failed to read signing key with error:
- 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
- Token signature could not be validated using the provided ke
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/699728223af980ec.
Report an issue: GitHub.