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
- 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
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
- 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
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
- Client id [ ] in logout request does not match client id […
- Unable to use 'none' as ID token signing algorithm
- Unable to use 'none' as ID token encryption algorithm
- No (successful) logout response received from the url
- Authentication request does not include the
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)