apereo/cas · error · CredentialsException
Client Credentials provided is not valid for service:
Error message
Client Credentials provided is not valid for service:
What it means
PKCE authenticator variant of client secret validation: before checking the code verifier, validateCredentials resolves the client secret from the request (via resolveClientIdAndClientSecret) and checks it against the registered service. A failed OAuth20ClientSecretValidator check throws this CredentialsException naming the service, rejecting the token request outright.
Solutions
- Ensure the client sends client_id and client_secret on the token request (Basic auth or form body) matching the registry entry
- Update the registered service's clientSecret to the client's current secret after any rotation
- Verify the service's secret-handling configuration (plain vs jose/JWT secret) matches what the client sends
- Inspect resolveClientIdAndClientSecret behavior — for PKCE public clients consider enabling PKCE-only validation without client secret
Example fix
// token request before POST /cas/oauth2.0/token grant_type=authorization_code&code=...&code_verifier=... // after POST /cas/oauth2.0/token grant_type=authorization_code&code=...&code_verifier=...&client_id=myapp&client_secret=correct-secret
Defensive patterns
Strategy: validation
Validate before calling
// client preflight: ensure secret is attached to the PKCE token exchange
if (!tokenRequest.containsParameter("client_secret") && !usesBasicAuth(tokenRequest)) {
throw new Error("Confidential PKCE clients must send client_secret on the token request");
} Try / catch
try {
authenticator.validate(credentials, context);
} catch (CredentialsException e) {
logger.warn("PKCE client credentials rejected: {}", e.getMessage());
throw e; // respond 401 invalid_client
} Prevention
- Keep client_secret in sync between client apps and the CAS service registry
- Include the Basic auth header or form client_secret on every token request for confidential clients
- Decide explicitly whether the client is public (PKCE-only) or confidential (secret + PKCE) and configure CAS accordingly
When it happens
Trigger: A token request using a PKCE authorization code whose client_secret (from Basic auth header or POST body, resolved by the request parameter resolver) fails clientSecretValidator.validate for the registered OAuth service.
Common situations: Confidential clients forgetting the client_secret on the token exchange; secret rotation mismatch between client and CAS service registry; public clients mistakenly configured with secret validation expectations; Basic auth header with incorrect Base64 or URL encoding.
Related errors
- Invalid client credentials provided for registered service:
- Code verification method is unrecognized:
- Invalid token:
- Code verification does not match the challenge assigned to:
- invalid_client
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/4cc374dba880e066.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java:77
}
throw new CredentialsException("Code verification method is unrecognized: " + method);
}
@Override
protected boolean canAuthenticate(final CallContext callContext) {
val context = callContext.webContext();
return getRequestParameterResolver().resolveRequestParameter(context, OAuth20Constants.CODE_VERIFIER).isPresent()
&& getRequestParameterResolver().resolveRequestParameter(context, OAuth20Constants.CODE).isPresent();
}
@Override
protected void validateCredentials(final UsernamePasswordCredentials credentials,
final OAuthRegisteredService registeredService,
final CallContext callContext,
final OAuth20ClientAuthenticationMethods authnMethod) {
val clientSecret = getRequestParameterResolver().resolveClientIdAndClientSecret(callContext).getRight();
if (!getClientSecretValidator().validate(registeredService, clientSecret)) {
throw new CredentialsException("Client Credentials provided is not valid for service: " + registeredService.getName());
}
val codeVerifier = getRequestParameterResolver()
.resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE_VERIFIER)
.map(String::valueOf).orElse(StringUtils.EMPTY);
val code = getRequestParameterResolver()
.resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE)
.map(String::valueOf).orElse(StringUtils.EMPTY);
LOGGER.debug("Received PKCE code verifier [{}] along with code [{}]", codeVerifier, code);
val token = getTicketRegistry().getTicket(code, OAuth20Code.class);
if (token == null || token.isExpired()) {
LOGGER.error("Provided code [{}] is either not found in the ticket registry or has expired", code);
throw new CredentialsException("Invalid token: " + code);
}
val method = StringUtils.defaultIfEmpty(token.getCodeChallengeMethod(), "plain");
val hash = calculateCodeVerifierHash(method, codeVerifier);
if (!hash.equalsIgnoreCase(token.getCodeChallenge())) {View on GitHub (pinned to e7288fc434)