{"record":{"id":"1b57306042ac6097","repo":"apereo/cas","slug":"token-s-has-an-invalid-issuer-s-that-does-not-ma","errorCode":null,"errorMessage":"Token %s has an invalid issuer %s that does not match %s","messagePattern":"Token (.+?) has an invalid issuer (.+?) that does not match (.+?)","errorType":"exception","errorClass":"AuthenticationException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-qr-authentication/src/main/java/org/apereo/cas/qr/validation/DefaultQRAuthenticationTokenValidatorService.java","lineNumber":60,"sourceCode":"\n        val now = LocalDateTime.now(Clock.systemUTC());\n        if (now.isAfter(dt)) {\n            LOGGER.trace(\"Comparing now at [{}] with token's expiration time [{}]\", now, dt);\n            throw new AuthenticationException(String.format(\"Token %s has expired\", tgt.getId()));\n        }\n\n        val authentication = tgt.getAuthentication();\n        LOGGER.trace(\"Authentication attempt linked to [{}] is [{}]\", tgt.getId(), authentication);\n\n        if (!authentication.getPrincipal().getId().equals(claims.getSubject())) {\n            val message = String.format(\"Token %s does not belong to the assigned principal\", claims.getSubject());\n            throw new AuthenticationException(message);\n        }\n\n        if (!claims.getIssuer().equals(casProperties.getServer().getPrefix())) {\n            val message = String.format(\"Token %s has an invalid issuer %s that does not match %s\", tgt.getId(),\n                claims.getIssuer(), casProperties.getServer().getPrefix());\n            throw new AuthenticationException(message);\n        }\n\n        val tokenDeviceId = FunctionUtils.doUnchecked(() -> claims.getStringClaim(QRAuthenticationConstants.QR_AUTHENTICATION_DEVICE_ID));\n        if (!Strings.CI.equals(tokenDeviceId, request.getDeviceId())) {\n            LOGGER.warn(\"Request device identifier [{}] does not match the token's identifier: [{}]\", request.getDeviceId(), tokenDeviceId);\n            throw new AuthenticationException(\"Request is assigned an invalid device identifier\");\n        }\n\n        if (!deviceRepository.isAuthorizedDeviceFor(request.getDeviceId(), claims.getSubject())) {\n            val message = String.format(\"Token is not authorized for device identifier [%s]\", request.getDeviceId());\n            throw new AuthenticationException(message);\n        }\n\n        return QRAuthenticationTokenValidationResult.builder()\n            .authentication(authentication)\n            .build();\n    }\n}","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-qr-authentication/src/main/java/org/apereo/cas/qr/validation/DefaultQRAuthenticationTokenValidatorService.java#L42-L78","documentation":"AuthenticationException thrown by DefaultQRAuthenticationTokenValidatorService.validate when the JWT issuer claim does not equal the configured CAS server prefix (cas.server.prefix). The issuer binds the token to the CAS deployment that created it.","triggerScenarios":"claims.getIssuer() != casProperties.getServer().getPrefix() during QR token validation.","commonSituations":"cas.server.prefix changed (domain move, http->https, context path added) after tokens were minted; load balancer proxies rewriting the external URL so the issuer recorded at mint time differs; QR token minted by a staging environment and validated in production; trailing-slash mismatch in the configured prefix.","solutions":["Align cas.server.prefix with the externally reachable CAS base URL used when tokens are minted (including scheme, host, port, and context path, exactly).","Ensure reverse proxies forward the original Host/X-Forwarded-* headers so the server prefix resolves identically at mint and validate time.","Refresh the QR code after any server-prefix configuration change; old tokens keep the old issuer.","Verify you are not mixing environments (staging QR with production CAS)."],"exampleFix":"// before\ncas.server.prefix=https://old.example.org/cas\n// after\ncas.server.prefix=https://cas.example.org/cas","handlingStrategy":"validation","validationCode":"// Compare the token issuer to the configured prefix before validating\nif (!claims.getIssuer().equals(serverPrefix)) {\n    throw new IllegalStateException(\"issuer mismatch: \" + claims.getIssuer() + \" vs \" + serverPrefix);\n}","typeGuard":null,"tryCatchPattern":"try { validatorService.validate(request); } catch (AuthenticationException e) { if (e.getMessage().contains(\"invalid issuer\")) { checkServerPrefixConfig(); } throw e; }","preventionTips":["Keep cas.server.prefix identical across all nodes and aligned with the external URL.","Configure reverse proxies to forward original host headers.","Re-mint QR tokens after any base-URL or context-path change."],"tags":["jwt","issuer-mismatch","configuration","qr-authentication"],"backgroundTag":"invalid-config-value","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}