apereo/cas · error

invalid_request

invalid_request

Error message

CAS cannot accept the request given the issuer is invalid.

What it means

OidcVerifiableCredentialEndpointController.verifyRequest validates the request's issuer against the configured issuer service for the VC credential URL path. When the Host/issuer of the incoming request does not match the configured OIDC issuer, the endpoint returns HTTP 400 with error 'invalid_request' and description 'Invalid issuer'.

Solutions

  1. Access the endpoint using the exact URL that matches the configured cas.authn.oidc.issuer.
  2. Configure the proxy/load balancer to preserve Host and X-Forwarded-* headers.
  3. Update the issuer configuration to include the scheme/host/path clients actually use.

Example fix

// before (request through wrong host)
curl https://localhost:8443/cas/oidc/vc/credential
// after
# with issuer=https://sso.example.org/cas/oidc
curl https://sso.example.org/cas/oidc/vc/credential -H "Authorization: Bearer ..."
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: ensure request URL starts with the configured issuer
String issuer = "https://sso.example.org/cas/oidc";
if (!requestUrl.startsWith(issuer)) {
    throw new IllegalArgumentException("Request must use the configured issuer URL " + issuer);
}

Try / catch

// Detect the 400 invalid_request body
Response resp = client.send(req);
if (resp.status() == 400 && body.contains("Invalid issuer")) {
    throw new IllegalStateException("Issuer mismatch: use " + configuredIssuer);
}

Prevention

When it happens

Trigger: Calling the verifiable-credential issuance endpoint with a request whose host/origin does not match cas.authn.oidc.issuer (issuerService.validateIssuer fails for OidcConstants.VC_CREDENTIAL_URL).

Common situations: Proxying behind a gateway that rewrites the Host header; accessing the server via localhost/IP instead of the configured issuer hostname; HTTP vs HTTPS mismatch; issuer configured with trailing path differences.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/oidc/vc/issuer/web/OidcVerifiableCredentialEndpointController.java:165

            nonces.add(issuedCredential.nonce());
            responses.add(OidcVerifiableCredentialResponse
                .builder()
                .format(issuedCredential.format().getValue())
                .credential(issuedCredential.credential())
                .build());
        }
        nonces.forEach(oidcVerifiableCredentialNonceService::remove);
        return responses.size() == 1
            ? ResponseEntity.ok(responses.getFirst())
            : ResponseEntity.ok(Map.of("credential_responses", responses));
    }

    protected Couplet<@Nullable OAuth20AccessToken, @Nullable ResponseEntity> verifyRequest(
        final HttpServletRequest httpRequest,
        final HttpServletResponse httpResponse) {
        val webContext = new JEEContext(httpRequest, httpResponse);
        if (!getConfigurationContext().getIssuerService().validateIssuer(webContext, List.of(OidcConstants.VC_CREDENTIAL_URL))) {
            LOGGER.warn("CAS cannot accept the request given the issuer is invalid.");
            val body = OAuth20Utils.getErrorResponseBody(OAuth20Constants.INVALID_REQUEST, "Invalid issuer");
            return Couplet.right(ResponseEntity.badRequest().body(body));
        }

        val decodedAccessTokenId = getAccessTokenFromRequest(httpRequest).getValue();
        val decodedToken = getConfigurationContext().getTicketRegistry().getTicket(decodedAccessTokenId, OAuth20AccessToken.class);
        if (!validateAccessToken(decodedToken)) {
            LOGGER.warn("The access token is invalid, expired, has an invalid grant type or no authorization details.");
            return Couplet.right(ResponseEntity.badRequest()
                .body(OAuth20Utils.getErrorResponseBody(OAuth20Constants.ERROR, "Invalid access token")));
        }
        return Couplet.left(decodedToken);
    }

    protected boolean validateAccessToken(@Nullable final OAuth20AccessToken accessToken) {
        return accessToken != null && !accessToken.isExpired()
            && (accessToken.getGrantType() == OAuth20GrantTypes.PRE_AUTHORIZED_CODE || accessToken.hasAuthorizationDetails());
    }

View on GitHub (pinned to e7288fc434)