thephpleague/oauth2-server · error · OAuthServerException

client authentication failed

Error message

client authentication failed

What it means

This error is thrown by AbstractGrant::validateClient when ClientRepository::validateClient() returns false, meaning the client credentials supplied with the token request (client_id/client_secret, from body or HTTP Basic auth) do not authenticate for the given grant type. The library emits a CLIENT_AUTHENTICATION_FAILED event and then throws OAuthServerException::invalidClient, surfacing as HTTP 401 with error 'invalid_client'. It is the library's way of rejecting failed confidential-client authentication during respondToAccessTokenRequest.

Solutions

  1. Verify the client_id and client_secret values sent in the token request match what your ClientRepository::validateClient expects (check env vars, trailing whitespace, quotes).
  2. Inspect your ClientRepository::validateClient implementation and log why it returns false (unknown client, bad secret, grant not allowed, revoked client).
  3. If using HTTP Basic auth, confirm the Authorization header is 'Basic base64(urlencode(client_id):urlencode(client_secret))' and that your server does not strip it.
  4. Ensure the grant type being requested is enabled for that client in your repository lookup.
  5. Listen for the RequestEvent::CLIENT_AUTHENTICATION_FAILED event to log the client_id and add diagnostics.

Example fix

// before (custom repository rejecting everything)
public function validateClient($clientId, $clientSecret, $grantType): bool {
    return $this->clients[$clientId]['secret'] === $clientSecret; // fails when client unknown
}
// after
public function validateClient($clientId, $clientSecret, $grantType): bool {
    $client = $this->clients[$clientId] ?? null;
    if ($client === null) { return false; }
    return hash_equals($client['secret'], (string) $clientSecret)
        && in_array($grantType, $client['grant_types'], true);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// caller-side pre-check
if (empty($clientId) || empty($clientSecret)) {
    throw new \RuntimeException('client_id and client_secret must be set before token exchange');
}

Type guard

function hasClientCredentials(array $params): bool {
    return isset($params['client_id'], $params['client_secret'])
        && is_string($params['client_id']) && $params['client_id'] !== ''
        && is_string($params['client_secret']) && $params['client_secret'] !== '';
}

Try / catch

try {
    $token = $server->respondToAccessTokenRequest($request, $response);
} catch (OAuthServerException $e) {
    if ($e->getCode() === 4 || $e->getErrorType() === 'invalid_client') {
        // 401: log client_id, surface 'check client credentials' to caller
    }
    return $e->generateHttpResponse($response);
}

Prevention

When it happens

Trigger: Calling $server->respondToAccessTokenRequest() on a password, client_credentials, refresh_token (with secret required) or authorization_code grant when: the client_secret is wrong or missing; the client_id exists but the secret does not match; validateClient() in the custom ClientRepository explicitly returns false (e.g. the client is revoked, or the grant type is not in the client's allowed list); credentials sent via Basic auth header are mis-decoded or the header is malformed.

Common situations: Secrets rotated in production but not in the client app; env var for client secret missing/empty on deploy; storing secrets with trailing whitespace or quotes picked up from .env; client registered as public but used with a secret (or vice versa) so the repository's validateClient logic rejects it; custom ClientRepository implementations that return false instead of null for unknown clients.

Understand the failure class

Related errors


AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15). Data as JSON: /api/errors/c406b1bf46ed3f7d. Report an issue: GitHub.

Appendix: source

Thrown at src/Grant/AbstractGrant.php:164

     * Validate the client.
     *
     * @throws OAuthServerException
     */
    protected function validateClient(ServerRequestInterface $request): ClientEntityInterface
    {
        [$clientId, $clientSecret] = $this->getClientCredentials($request);

        $client = $this->getClientEntityOrFail($clientId, $request);

        if ($client->isConfidential()) {
            if ($clientSecret === '') {
                throw OAuthServerException::invalidRequest('client_secret');
            }

            if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
                $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));

                throw OAuthServerException::invalidClient($request);
            }
        }

        return $client;
    }

    /**
     * Wrapper around ClientRepository::getClientEntity() that ensures we emit
     * an event and throw an exception if the repo doesn't return a client
     * entity.
     *
     * This is a bit of defensive coding because the interface contract
     * doesn't actually enforce non-null returns/exception-on-no-client so
     * getClientEntity might return null. By contrast, this method will
     * always either return a ClientEntityInterface or throw.
     *
     * @throws OAuthServerException
     */

View on GitHub (pinned to 9d2f6fc0a0)