apereo/cas · error

invalid_client

invalid_client

Error message

Unable to locate and extract credentials from the request

What it means

The OAuth 2.0 introspection endpoint could not extract client credentials (client id / secret) from the HTTP POST request. CAS looks for Basic Auth headers or client_id/client_secret form parameters via extractCredentials(); when neither is present, it rejects the call with an OAuth 'invalid_client' error and HTTP 401.

Solutions

  1. Send the client id and secret as HTTP Basic Authorization on the introspection POST request
  2. Alternatively POST client_id and client_secret as application/x-www-form-urlencoded body parameters
  3. Verify no proxy or gateway strips the Authorization header
  4. Ensure the Content-Type is application/x-www-form-urlencoded so parameters are parsed

Example fix

// before
curl -X POST https://cas.example.org/cas/oauth2.0/introspect -d 'token=AT-123'
// after
curl -u myClient:mySecret -X POST https://cas.example.org/cas/oauth2.0/introspect -d 'token=AT-123'
Defensive patterns

Strategy: validation

Validate before calling

// client-side pre-check before calling the endpoint
const hasBasic = config.clientId && config.clientSecret;
const hasForm = !!config.formParams?.client_id;
if (!hasBasic && !hasForm) {
  throw new Error('Introspection requires Basic auth or client_id/client_secret form params');
}

Try / catch

try {
  const res = await fetch(introspectUrl, { method: 'POST', headers: { Authorization: 'Basic ' + btoa(id + ':' + secret) } });
  if (res.status === 401 && (await res.text()).includes('invalid_client')) {
    // credentials were missing/unparseable: fix auth header before retrying
  }
} catch (e) { /* network failure */ }

Prevention

When it happens

Trigger: Calling POST to the introspection endpoint without an Authorization: Basic header and without client_id/client_secret form parameters, or with credentials that extractCredentials() cannot parse (e.g. malformed Basic base64, wrong Content-Type so form params are not read).

Common situations: Client apps omitting the Basic auth header after a framework upgrade; sending credentials as a JSON body instead of form-encoded parameters; reverse proxies stripping the Authorization header; typo in the parameter names.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/web/endpoints/OAuth20IntrospectionEndpointController.java:99

    }

    /**
     * Handle post request.
     *
     * @param request  the request
     * @param response the response
     * @return the response entity
     * @throws Throwable the throwable
     */
    @PostMapping(value = OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.INTROSPECTION_URL,
        produces = MediaType.APPLICATION_JSON_VALUE)
    @Operation(summary = "Handle OAuth introspection request")
    public ResponseEntity handlePostRequest(final HttpServletRequest request, final HttpServletResponse response) throws Throwable {
        try {
            val context = new JEEContext(request, response);
            val credentialsResult = extractCredentials(context);
            if (credentialsResult.isEmpty()) {
                LOGGER.warn("Unable to locate and extract credentials from the request");
                return buildUnauthorizedResponseEntity(OAuth20Constants.INVALID_CLIENT, true);
            }

            val credentials = (UsernamePasswordCredentials) credentialsResult.get();
            val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(
                getConfigurationContext().getServicesManager(), credentials.getUsername());
            if (registeredService == null) {
                LOGGER.warn("Unable to locate service definition by client id [{}]", credentials.getUsername());
                return buildUnauthorizedResponseEntity(OAuth20Constants.INVALID_CLIENT, true);
            }

            val validationError = validateIntrospectionRequest(registeredService, credentials, request);
            if (validationError.isPresent()) {
                return validationError.get();
            }

            val tokenId = StringUtils.defaultIfBlank(request.getParameter(OAuth20Constants.TOKEN),
                request.getParameter(OAuth20Constants.ACCESS_TOKEN));

View on GitHub (pinned to e7288fc434)