apereo/cas · error
invalid_request
invalid_request
Error message
The presentation response could not be validated
What it means
OidcVerifiableCredentialPresentationResponseEndpointController.handleResponse validates a verifiable presentation submission (vp_token, credentials, nonce, transient session ticket) inside a try block. Any Throwable during validation results in HTTP 400 with error 'invalid_request' and message 'The presentation response could not be validated'; the transient session ticket is only deleted on success.
Solutions
- Re-run the authorization/presentation flow to get a fresh nonce and transient session ticket, then submit the response once.
- Verify the holder's presentation is signed with a supported algorithm and the credentials match the requested types.
- Ensure the nonce from the earlier response is included unchanged in the presentation.
- Inspect CAS logs (LoggingUtils warns with the underlying throwable) to identify the exact validation failure.
Example fix
// before: reusing a consumed ticket/nonce POST presentation-response with old vp_token + old nonce // after: restart flow 1) obtain new nonce + transient ticket 2) build fresh vp_token including that nonce 3) POST once
Defensive patterns
Strategy: try-catch
Validate before calling
// Client-side sanity checks before submitting the presentation
if (vpToken == null || nonce == null || transientTicketId == null) {
throw new IllegalArgumentException("vp_token, nonce and transient ticket are required");
} Try / catch
try {
Response resp = submitPresentation(vpToken, credentials, nonce, ticketId);
if (resp.status() == 400 && body.contains("could not be validated")) {
restartFlow(); // new nonce + ticket, fresh presentation
}
} catch (IOException e) {
LOGGER.warn("Presentation submission failed", e);
} Prevention
- Submit the presentation response exactly once; tickets are single-use
- Include the issued nonce verbatim in the presentation
- Keep holder keys and algorithms within CAS-supported set
- Restart the flow on any 400 rather than retrying the same payload
When it happens
Trigger: Submitting a presentation response whose vp_token signature/proof fails verification, credentials don't match the requested types, the nonce doesn't match the issued one, or the transient session ticket is missing/expired — any exception in validatePresentation.
Common situations: Wallet replaying an old nonce; clock skew invalidating credential validity; unsupported proof/algorithm in the holder's presentation; user retrying after the transient ticket was already consumed or expired.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Missing required principal attribute for claim
- Redirect URI cannot contain a fragment
- Invalid sector identifier uri
- Invalid logo uri from an unknown host
- Invalid policy uri from an unknown host
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/25bf62839f6e3a1f.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/vc/presentation/OidcVerifiableCredentialPresentationResponseEndpointController.java:111
@RequestParam final String state) {
try {
require(!vpToken.isBlank() && !state.isBlank(), "Presentation response parameters cannot be blank");
val transientSessionTicket = configurationContext.getTicketRegistry().getTicket(state, TransientSessionTicket.class);
require(transientSessionTicket != null && !transientSessionTicket.isExpired(), "Presentation transaction is invalid");
require(state.equals(transientSessionTicket.getPropertyAsString("state")), "Presentation state does not match");
val nonce = transientSessionTicket.getPropertyAsString("nonce");
require(nonce != null && !nonce.isBlank(), "Presentation transaction has no nonce");
val credentials = (List<CredentialRequest>) transientSessionTicket.getProperty("credentials", List.class);
require(credentials != null && !credentials.isEmpty(), "Presentation transaction has no credential query");
validatePresentation(vpToken, credentials, nonce, transientSessionTicket);
configurationContext.getTicketRegistry().deleteTicket(transientSessionTicket);
return buildResponse(HttpStatus.OK, Map.of("status", "verified"));
} catch (final Throwable throwable) {
LoggingUtils.warn(LOGGER, throwable);
return buildResponse(HttpStatus.BAD_REQUEST,
OAuth20Utils.getErrorResponseBody(OAuth20Constants.INVALID_REQUEST,
"The presentation response could not be validated")
);
}
}
private void validatePresentation(final String vpToken,
final List<CredentialRequest> credentials,
final String nonce,
final TransientSessionTicket transientSessionTicket) throws Throwable {
val credentialQueries = new LinkedHashMap<String, CredentialRequest>();
for (val credential : credentials) {
require(credential != null && credential.getId() != null && !credential.getId().isBlank(),
"Credential query id is invalid");
require(OidcVerifiableCredentialConfigurationProperties.CredentialConfigurationFormats.DC_SD_JWT
.getValue().equals(credential.getFormat()), "Credential query format is not supported");
require(credential.getVctValues() != null && !credential.getVctValues().isEmpty()View on GitHub (pinned to e7288fc434)