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

  1. 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
  2. Fix trailing-slash or scheme mismatches: configure the issuer exactly as the IdP emits it in its discovery document (metadata.issuer)
  3. Confirm tokens come from the intended IdP/realm; if validating multiple issuers, use the per-issuer validator for each token
  4. 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

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


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