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

  1. Ensure the client sends client_id and client_secret on the token request (Basic auth or form body) matching the registry entry
  2. Update the registered service's clientSecret to the client's current secret after any rotation
  3. Verify the service's secret-handling configuration (plain vs jose/JWT secret) matches what the client sends
  4. 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

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


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)