{"record":{"id":"19e941e6e4311fee","repo":"roundcube/roundcubemail","slug":"failed-to-validate-jwt-issuer-mismatch","errorCode":null,"errorMessage":"Failed to validate JWT: issuer mismatch","messagePattern":"Failed to validate JWT: issuer mismatch","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"program/include/rcmail_oauth.php","lineNumber":457,"sourceCode":"            [$headb64, $bodyb64, $cryptob64] = explode('.', $jwt);\n\n            $header = json_decode(static::base64url_decode($headb64), true);\n            $body = json_decode(static::base64url_decode($bodyb64), true);\n            // $crypto = static::base64url_decode($cryptob64);\n        }\n\n        // FIXME depends on body type: ID, Logout, Bearer, Refresh,\n        if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {\n            throw new \\RuntimeException('Failed to validate JWT: invalid azp value');\n        } elseif (isset($body['aud']) && !in_array($this->options['client_id'], (array) $body['aud'])) {\n            throw new \\RuntimeException('Failed to validate JWT: invalid aud value');\n        } elseif (!isset($body['azp']) && !isset($body['aud'])) {\n            throw new \\RuntimeException('Failed to validate JWT: missing aud/azp value');\n        }\n\n        // if defined in parameters, check that issuer match\n        if (isset($this->options['issuer']) && $body['iss'] !== $this->options['issuer']) {\n            throw new \\RuntimeException('Failed to validate JWT: issuer mismatch');\n        }\n\n        // check that token is not an outdated message\n        if (isset($body['exp']) && (time() > $body['exp'])) {\n            throw new \\RuntimeException('Failed to validate JWT: expired message');\n        }\n\n        $body['header'] = $header;\n\n        $this->log_debug('jwt: %s', json_encode($body));\n\n        return $body;\n    }\n\n    /**\n     * Compose a fully qualified redirect URI for auth requests\n     *\n     * @return string","sourceCodeStart":439,"sourceCodeEnd":475,"githubUrl":"https://github.com/roundcube/roundcubemail/blob/4b54c2acfb54d5ee3d1c281ca7f143bed0dea804/program/include/rcmail_oauth.php#L439-L475","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$config['oauth_issuer'] = 'https://idp.example.com/realms/old';\n// after (token iss)\n$config['oauth_issuer'] = 'https://idp.example.com/realms/main';","handlingStrategy":"validation","validationCode":"$claims = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/')), true);\nif (isset($claims['iss']) && rtrim($claims['iss'], '/') !== rtrim(OAUTH_ISSUER, '/')) { /* abort: issuer mismatch */ }","typeGuard":"function issuerMatches(array $claims, string $expected): bool { return !isset($claims['iss']) || rtrim($claims['iss'], '/') === rtrim($expected, '/'); }","tryCatchPattern":"try { $payload = $oauth->jwt_decode($token); } catch (\\RuntimeException $e) { // re-run discovery or re-authenticate }","preventionTips":["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."],"tags":["oauth","jwt","oidc","issuer-mismatch"],"backgroundTag":"invalid-config-value","analyzedSha":"4b54c2acfb54d5ee3d1c281ca7f143bed0dea804","analyzedAt":"2026-09-14T13:23:19.231Z","contentChangedAt":"2026-09-14T13:23:19.231Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}