roundcube/roundcubemail · error · RuntimeException
Failed to validate JWT: issuer mismatch
Error message
Failed to validate JWT: issuer mismatch
What it means
When the 'issuer' option is configured, jwt_decode() compares it with the token's 'iss' claim. A mismatch means the token came from a different authority than the configured provider and cannot be trusted for this instance.
Solutions
- Decode the token and copy the exact 'iss' value into the Roundcube 'oauth_issuer' config option.
- Check for trailing-slash or scheme (http/https) differences between config and token.
- Ensure tokens come from the configured realm/tenant, not another one.
- Re-run discovery/discovery cache clear so issuer and well-known URL are consistent.
Example fix
// before $config['oauth_issuer'] = 'https://idp.example.com/realms/old'; // after (token iss) $config['oauth_issuer'] = 'https://idp.example.com/realms/main';
Defensive patterns
Strategy: validation
Validate before calling
$claims = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/')), true);
if (isset($claims['iss']) && rtrim($claims['iss'], '/') !== rtrim(OAUTH_ISSUER, '/')) { /* abort: issuer mismatch */ } Type guard
function issuerMatches(array $claims, string $expected): bool { return !isset($claims['iss']) || rtrim($claims['iss'], '/') === rtrim($expected, '/'); } Try / catch
try { $payload = $oauth->jwt_decode($token); } catch (\RuntimeException $e) { // re-run discovery or re-authenticate } Prevention
- Copy 'iss' verbatim from a real token into oauth_issuer config.
- Watch for trailing slashes and http/https scheme differences.
- After IDP realm/tenant changes, update Roundcube config and clear the discovery cache.
When it happens
Trigger: parse_tokens() -> jwt_decode() where isset($options['issuer']) and $body['iss'] !== options['issuer'] — e.g. issuer URL trailing-slash differences, http vs https, hostname vs internal name, or realm changed on the IDP.
Common situations: Keycloak realm rename/realm path change; switching between localhost and production issuer; trailing slash mismatch ('https://idp/x/' vs 'https://idp/x'); multi-tenant providers returning per-tenant issuer values.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- Failed to validate JWT: invalid azp value
- Failed to validate JWT: invalid aud value
- Failed to validate JWT: missing aud/azp value
- Failed to validate JWT: expired message
- OIDC: Handle only logout events
AI-assisted analysis of roundcube/roundcubemail@4b54c2acfb (2026-09-14).
Data as JSON: /api/errors/19e941e6e4311fee.
Report an issue: GitHub.
Appendix: source
Thrown at program/include/rcmail_oauth.php:457
[$headb64, $bodyb64, $cryptob64] = explode('.', $jwt);
$header = json_decode(static::base64url_decode($headb64), true);
$body = json_decode(static::base64url_decode($bodyb64), true);
// $crypto = static::base64url_decode($cryptob64);
}
// FIXME depends on body type: ID, Logout, Bearer, Refresh,
if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {
throw new \RuntimeException('Failed to validate JWT: invalid azp value');
} elseif (isset($body['aud']) && !in_array($this->options['client_id'], (array) $body['aud'])) {
throw new \RuntimeException('Failed to validate JWT: invalid aud value');
} elseif (!isset($body['azp']) && !isset($body['aud'])) {
throw new \RuntimeException('Failed to validate JWT: missing aud/azp value');
}
// if defined in parameters, check that issuer match
if (isset($this->options['issuer']) && $body['iss'] !== $this->options['issuer']) {
throw new \RuntimeException('Failed to validate JWT: issuer mismatch');
}
// check that token is not an outdated message
if (isset($body['exp']) && (time() > $body['exp'])) {
throw new \RuntimeException('Failed to validate JWT: expired message');
}
$body['header'] = $header;
$this->log_debug('jwt: %s', json_encode($body));
return $body;
}
/**
* Compose a fully qualified redirect URI for auth requests
*
* @return stringView on GitHub (pinned to 4b54c2acfb)