apereo/cas · error · ResponseStatusException

screen.oidc.issuer.invalid

screen.oidc.issuer.invalid

Error message

The issuer URL is invalid and does not match the CAS server issuer URL.

What it means

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.

Solutions

  1. 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
  2. Request a fresh ID token from the correct client before logout
  3. Remove the mismatched client_id parameter if it is optional and let CAS derive the client from the ID token

Example fix

// before
GET /oidc/oidcLogout?id_token_hint=<token-for-appA>&client_id=appB
// after
GET /oidc/oidcLogout?id_token_hint=<token-for-appA>&client_id=appA
Defensive patterns

Strategy: validation

Validate before calling

String tokenClientId = OAuth20Utils.extractClientIdFromToken(idToken);
if (givenClientId != null && !givenClientId.equalsIgnoreCase(tokenClientId)) {
    throw new IllegalArgumentException("client_id does not match ID token client");
}

Try / catch

try { controller.handleRequestInternal(request, response); }
catch (ResponseStatusException e) {
    if (e.getStatusCode() == HttpStatus.BAD_REQUEST && "screen.oidc.issuer.invalid".equals(e.getReason())) { /* align client_id with the ID token */ }
    else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/5f51b05a609634c0. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-oidc-core-api/src/main/java/org/apereo/cas/oidc/web/controllers/logout/OidcLogoutEndpointController.java:104

        @RequestParam(value = OidcConstants.POST_LOGOUT_REDIRECT_URI, required = false)
        final String postLogoutRedirectUrl,
        @RequestParam(value = OAuth20Constants.STATE, required = false)
        final String state,
        @RequestParam(value = OAuth20Constants.CLIENT_ID, required = false)
        final String givenClientId,
        @RequestParam(value = OidcConstants.ID_TOKEN_HINT, required = false)
        final String idToken,
        final HttpServletRequest request, final HttpServletResponse response) throws Throwable {

        if (StringUtils.isNotBlank(idToken)) {
            LOGGER.trace("Decoding logout ID token [{}]", idToken);

            val clientIdInIdToken = OAuth20Utils.extractClientIdFromToken(idToken);
            LOGGER.debug("Client id retrieved from ID token is [{}]", clientIdInIdToken);

            if (StringUtils.isNotBlank(givenClientId) && !Strings.CI.equals(givenClientId, clientIdInIdToken)) {
                LOGGER.warn("Client id [{}] in logout request does not match client id [{}] in ID token", givenClientId, clientIdInIdToken);
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    configurationContext.getMessageSource().getMessage("screen.oidc.issuer.invalid", ArrayUtils.EMPTY_OBJECT_ARRAY, request.getLocale()));
            }
            val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(
                getConfigurationContext().getServicesManager(), clientIdInIdToken, OidcRegisteredService.class);
            val idTokenClaims = getConfigurationContext().getIdTokenSigningAndEncryptionService().decode(idToken, Optional.of(registeredService));
            Assert.isTrue(idTokenClaims.getClaimValueAsString(OAuth20Constants.CLIENT_ID).equalsIgnoreCase(registeredService.getClientId()),
                "Client id in ID token does not match client id in registered service");
            Assert.isTrue(idTokenClaims.hasClaim(OidcConstants.AUD), "Audience claim is not present");
            Assert.isTrue(idTokenClaims.hasClaim(OAuth20Constants.CLAIM_SUB), "Subject claim is not present");

            LOGGER.debug("Located registered service [{}]", registeredService);
            val service = getConfigurationContext().getWebApplicationServiceServiceFactory().createService(clientIdInIdToken);
            val audit = AuditableContext.builder()
                .service(service)
                .registeredService(registeredService)
                .build();
            val accessResult = getConfigurationContext().getRegisteredServiceAccessStrategyEnforcer().execute(audit);
            accessResult.throwExceptionIfNeeded();

View on GitHub (pinned to e7288fc434)