apereo/cas · error · CredentialsException

Invalid client credentials provided for registered service:

Error message

Invalid client credentials provided for registered service: 

What it means

OAuth20ClientIdClientSecretAuthenticator validates OAuth2 client authentication (basic auth or POST body client_id/client_secret) against the registered service's stored secret via OAuth20ClientSecretValidator. When the presented secret does not match, it throws pac4j CredentialsException naming the registered service, rejecting client authentication.

Solutions

  1. Compare the client's sent secret with the registered service's clientSecret in the service registry (JSON/YAML) and update whichever is stale
  2. If the service uses a signed/JWT secret, ensure the client computes and sends the secret per that method (jose-signed vs plain)
  3. Check for copy/paste artifacts (leading/trailing spaces, quotes) in both the client config and registry entry
  4. Confirm the client is sending credentials correctly: Basic auth header or POST parameters client_id/client_secret, properly URL-encoded

Example fix

// service registry before
"clientId": "myapp", "clientSecret": "old-secret"

// after  (sync with the client after rotation)
"clientId": "myapp", "clientSecret": "new-secret"
Defensive patterns

Strategy: validation

Validate before calling

// verify registry secret matches client config before deploy
OAuthRegisteredService svc = servicesManager.findServiceBy(clientId);
if (svc != null && !svc.getClientSecret().equals(expectedClientSecret)) {
    throw new IllegalStateException("Client secret mismatch for service " + svc.getName());
}

Try / catch

try {
    authenticator.validate(credentials, context);
} catch (CredentialsException e) {
    logger.warn("OAuth client auth rejected: {}", e.getMessage());
    throw e; // surfaces as 401 invalid_client
}

Prevention

When it happens

Trigger: validateCredentials is invoked by the pac4j validate() flow when a client presents client_id/client_secret; clientSecretValidator.validate(registeredService, pwdToCheck) returns false — the secret is wrong, or (for CLIENT_SECRET_BASIC) the URL-decoded password still doesn't match.

Common situations: Client rotated its secret but the CAS service registry still holds the old one; percent-encoded secrets in HTTP Basic auth not matching after decode; whitespace/quote characters pasted into the registered service's clientSecret; JWT-signed vs plain secret mismatch (service configured as jwt-secret but client sends plain).

Related errors


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

Appendix: source

Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ClientIdClientSecretAuthenticator.java:181

        val finalPrincipal = profileScopeToAttributesFilter.filter(service, resolvedPrincipal, registeredService, accessToken);
        LOGGER.debug("Built final principal [{}]", finalPrincipal);
        return finalPrincipal;
    }

    protected Collection<String> resolveRequestedScopes(final CallContext callContext) {
        return requestParameterResolver.resolveRequestedScopes(callContext.webContext());
    }

    protected void validateCredentials(final UsernamePasswordCredentials credentials,
                                       final OAuthRegisteredService registeredService,
                                       final CallContext callContext,
                                       final OAuth20ClientAuthenticationMethods authnMethod) {
        var pwdToCheck = credentials.getPassword();
        if (authnMethod == OAuth20ClientAuthenticationMethods.CLIENT_SECRET_BASIC) {
            pwdToCheck = EncodingUtils.urlDecode(credentials.getPassword());
        }
        if (!clientSecretValidator.validate(registeredService, pwdToCheck)) {
            throw new CredentialsException("Invalid client credentials provided for registered service: " + registeredService.getName());
        }
    }

    protected boolean canAuthenticate(final CallContext callContext) {
        val context = callContext.webContext();
        val grantType = requestParameterResolver.resolveGrantType(context);

        if (grantType == OAuth20GrantTypes.PASSWORD) {
            LOGGER.debug("Skipping client credential authentication to use password authentication");
            return false;
        }

        val clientIdAndSecret = requestParameterResolver.resolveClientIdAndClientSecret(callContext);
        if (grantType == OAuth20GrantTypes.REFRESH_TOKEN
            && StringUtils.isNotBlank(clientIdAndSecret.getKey())
            && StringUtils.isBlank(clientIdAndSecret.getValue())) {
            LOGGER.debug("Skipping client credential authentication to use refresh token authentication");
            return false;

View on GitHub (pinned to e7288fc434)