{"record":{"id":"5f51b05a609634c0","repo":"apereo/cas","slug":"screen-oidc-issuer-invalid","errorCode":"screen.oidc.issuer.invalid","errorMessage":"The issuer URL is invalid and does not match the CAS server issuer URL.","messagePattern":"The issuer URL is invalid and does not match the CAS server issuer URL\\.","errorType":"http","errorClass":"ResponseStatusException","httpStatus":400,"severity":"error","filePath":"support/cas-server-support-oidc-core-api/src/main/java/org/apereo/cas/oidc/web/controllers/logout/OidcLogoutEndpointController.java","lineNumber":104,"sourceCode":"        @RequestParam(value = OidcConstants.POST_LOGOUT_REDIRECT_URI, required = false)\n        final String postLogoutRedirectUrl,\n        @RequestParam(value = OAuth20Constants.STATE, required = false)\n        final String state,\n        @RequestParam(value = OAuth20Constants.CLIENT_ID, required = false)\n        final String givenClientId,\n        @RequestParam(value = OidcConstants.ID_TOKEN_HINT, required = false)\n        final String idToken,\n        final HttpServletRequest request, final HttpServletResponse response) throws Throwable {\n\n        if (StringUtils.isNotBlank(idToken)) {\n            LOGGER.trace(\"Decoding logout ID token [{}]\", idToken);\n\n            val clientIdInIdToken = OAuth20Utils.extractClientIdFromToken(idToken);\n            LOGGER.debug(\"Client id retrieved from ID token is [{}]\", clientIdInIdToken);\n\n            if (StringUtils.isNotBlank(givenClientId) && !Strings.CI.equals(givenClientId, clientIdInIdToken)) {\n                LOGGER.warn(\"Client id [{}] in logout request does not match client id [{}] in ID token\", givenClientId, clientIdInIdToken);\n                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,\n                    configurationContext.getMessageSource().getMessage(\"screen.oidc.issuer.invalid\", ArrayUtils.EMPTY_OBJECT_ARRAY, request.getLocale()));\n            }\n            val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(\n                getConfigurationContext().getServicesManager(), clientIdInIdToken, OidcRegisteredService.class);\n            val idTokenClaims = getConfigurationContext().getIdTokenSigningAndEncryptionService().decode(idToken, Optional.of(registeredService));\n            Assert.isTrue(idTokenClaims.getClaimValueAsString(OAuth20Constants.CLIENT_ID).equalsIgnoreCase(registeredService.getClientId()),\n                \"Client id in ID token does not match client id in registered service\");\n            Assert.isTrue(idTokenClaims.hasClaim(OidcConstants.AUD), \"Audience claim is not present\");\n            Assert.isTrue(idTokenClaims.hasClaim(OAuth20Constants.CLAIM_SUB), \"Subject claim is not present\");\n\n            LOGGER.debug(\"Located registered service [{}]\", registeredService);\n            val service = getConfigurationContext().getWebApplicationServiceServiceFactory().createService(clientIdInIdToken);\n            val audit = AuditableContext.builder()\n                .service(service)\n                .registeredService(registeredService)\n                .build();\n            val accessResult = getConfigurationContext().getRegisteredServiceAccessStrategyEnforcer().execute(audit);\n            accessResult.throwExceptionIfNeeded();","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-oidc-core-api/src/main/java/org/apereo/cas/oidc/web/controllers/logout/OidcLogoutEndpointController.java#L86-L122","documentation":"The OIDC logout endpoint validates the client identifier from the ID token against the post_logout_redirect / id_token_hint request. When a client id is supplied in the logout request and it does not match the client id extracted from the ID token, the controller aborts with HTTP 400 and the localized 'screen.oidc.issuer.invalid' message.","triggerScenarios":"handleRequestInternal(): the logout request carries a client_id (or post_logout_redirect_uri implies one) and StringUtils.isNotBlank(givenClientId) but givenClientId is not case-insensitively equal to OAuth20Utils.extractClientIdFromToken(idToken).","commonSituations":"Passing the wrong client_id query parameter during logout; testing with an ID token issued to a different application; copy-pasted logout URLs from another environment/client; renaming a client id while cached ID tokens still reference the old one.","solutions":["Make the client_id (or post_logout_redirect_uri's associated client) in the logout request match the aud/client_id claim of the ID token being presented","Request a fresh ID token from the correct client before logout","Remove the mismatched client_id parameter if it is optional and let CAS derive the client from the ID token"],"exampleFix":"// before\nGET /oidc/oidcLogout?id_token_hint=<token-for-appA>&client_id=appB\n// after\nGET /oidc/oidcLogout?id_token_hint=<token-for-appA>&client_id=appA","handlingStrategy":"validation","validationCode":"String tokenClientId = OAuth20Utils.extractClientIdFromToken(idToken);\nif (givenClientId != null && !givenClientId.equalsIgnoreCase(tokenClientId)) {\n    throw new IllegalArgumentException(\"client_id does not match ID token client\");\n}","typeGuard":null,"tryCatchPattern":"try { controller.handleRequestInternal(request, response); }\ncatch (ResponseStatusException e) {\n    if (e.getStatusCode() == HttpStatus.BAD_REQUEST && \"screen.oidc.issuer.invalid\".equals(e.getReason())) { /* align client_id with the ID token */ }\n    else throw e;\n}","preventionTips":["Build logout URLs from the same client configuration that issued the ID token","Never hardcode client ids in logout links across environments","Verify the id_token_hint belongs to the client before calling logout"],"tags":["oidc","logout","id-token","http-400","client-id-mismatch"],"backgroundTag":"invalid-argument-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"}