{"record":{"id":"3c64d926ee353eef","repo":"apereo/cas","slug":"client-id-in-logout-request-does-not-match-cl","errorCode":null,"errorMessage":"Client id [{}] in logout request does not match client id [{}] in ID token","messagePattern":"Client id \\[(.+?)\\] in logout request does not match client id \\[(.+?)\\] in ID token","errorType":"console","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":103,"sourceCode":"    public ResponseEntity handleRequestInternal(\n        @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);","sourceCodeStart":85,"sourceCodeEnd":121,"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#L85-L121","documentation":"During OIDC RP-initiated logout, CAS decodes the ID token and extracts its client_id claim. If the caller supplied a client_id parameter (or it was derived from the request) that differs from the client_id inside the ID token, CAS throws ResponseStatusException(HTTP 400) with the 'screen.oidc.issuer.invalid' message — the logout request is treated as tampered/mismatched.","triggerScenarios":"GET /oidc/oidcLogout (or logout) where the 'client_id' request parameter is non-blank and case-insensitively different from the client_id claim embedded in the posted/referenced id_token_hint.","commonSituations":"RPs storing a stale or different client id (renamed/re-registered service); passing the wrong client's id_token_hint; multiple client registrations with similar ids; hand-built logout URLs copying the wrong parameter.","solutions":["Ensure the client_id parameter exactly equals the aud/azp client in the id_token_hint","Regenerate the ID token from the correct client after re-registering/renaming services","Omit the client_id parameter when posting an id_token_hint if the RP cannot guarantee the match","Verify via OAuth20Utils.getRegisteredOAuthServiceByClientId that the intended service is registered and enabled"],"exampleFix":"// before\nGET /cas/oidc/oidcLogout?client_id=oldClient&id_token_hint=<token with azp=newClient>\n// after\nGET /cas/oidc/oidcLogout?client_id=newClient&id_token_hint=<token with azp=newClient>","handlingStrategy":"validation","validationCode":"const claims = decodeJwtClaims(idTokenHint);\nif (clientId && claims.azp !== clientId && !(claims.aud ?? []).includes(clientId)) {\n  throw new Error(`client_id ${clientId} does not match ID token aud/azp`);\n}","typeGuard":"function idTokenMatchesClient(idToken, clientId) {\n  const claims = decodeJwtClaims(idToken);\n  return claims.azp === clientId || (Array.isArray(claims.aud) ? claims.aud.includes(clientId) : claims.aud === clientId);\n}","tryCatchPattern":"try {\n  await cas.logout({ clientId, idTokenHint });\n} catch (e) {\n  if (e.status === 400) {\n    // client_id vs ID token mismatch: drop client_id or re-issue the token\n  }\n  throw e;\n}","preventionTips":["Store and send the same client id used to obtain the ID token","Re-fetch ID tokens after renaming/re-registering clients","Omit client_id when unsure and rely on id_token_hint alone","Keep service registrations' ids stable"],"tags":["oidc","logout","client-id","http-400","id-token"],"backgroundTag":"invalid-identifier","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"}