thephpleague/oauth2-server · error · OAuthServerException

unauthorized_client

unauthorized_client

Error message

unauthorized_client

What it means

Thrown by AbstractGrant::getClientEntityOrFail when the client entity was found but supportsGrantType($client, $this->getIdentifier()) returns false, i.e. the client is not authorized to use the requested grant type. It throws OAuthServerException::unauthorizedClient (HTTP 401, error code 'unauthorized_client'). The client authenticated fine; it simply is not permitted to run this grant.

Solutions

  1. Add the requested grant type (e.g. 'client_credentials', 'refresh_token') to the client's allowed grants in your client store.
  2. Check your ClientRepository/ClientEntity grant-type logic (supportsGrantType source) and make sure the grant identifier from $grant->getIdentifier() is included.
  3. Use a separate client record with the correct grant types for service-to-service (client_credentials) usage instead of reusing the web app client.
  4. If the grant list is stored as JSON/CSV, confirm the column is actually populated and parsed correctly.

Example fix

// before (client row)
{"id":"service-a","grants":["authorization_code"]}
// after — allow the grant actually being requested
{"id":"service-a","grants":["authorization_code","client_credentials"]}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the grant is allowed before requesting
$allowed = in_array('client_credentials', $clientConfig['grants'] ?? [], true);
if (!$allowed) {
    throw new \DomainException('client is not authorized for client_credentials grant');
}

Type guard

function supportsGrant(array $clientConfig, string $grantId): bool {
    return isset($clientConfig['grants'])
        && is_array($clientConfig['grants'])
        && in_array($grantId, $clientConfig['grants'], true);
}

Try / catch

try {
    $token = $server->respondToAccessTokenRequest($request, $response);
} catch (OAuthServerException $e) {
    if ($e->getErrorType() === 'unauthorized_client') {
        // 401: the client exists but may not use this grant; point admin at client grant config
    }
    return $e->generateHttpResponse($response);
}

Prevention

When it happens

Trigger: Requesting a client_credentials token with a client restricted to the authorization_code grant; using the refresh_token grant with a client whose allowed grant list omits it; calling validateAuthorizationRequest or respondToDeviceAuthorizationRequest for a client whose repository data lacks the requested grant identifier; a custom ClientEntity::getGrants()/repository check that returns an empty grant list.

Common situations: Registering clients without populating their allowed grant types; after a library upgrade the grant identifier set changed and stored client grant lists are stale; copying a client row for a new integration but forgetting to add the new grant; mixing up a public SPA client (authorization_code only) with a backend service client.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at src/Grant/AbstractGrant.php:193

     *
     * 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
     */
    protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
    {
        $client = $this->clientRepository->getClientEntity($clientId);

        if ($client instanceof ClientEntityInterface === false) {
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
            throw OAuthServerException::invalidClient($request);
        }

        if ($this->supportsGrantType($client, $this->getIdentifier()) === false) {
            throw OAuthServerException::unauthorizedClient();
        }

        return $client;
    }

    /**
     * Returns true if the given client is authorized to use the given grant type.
     */
    protected function supportsGrantType(ClientEntityInterface $client, string $grantType): bool
    {
        return method_exists($client, 'supportsGrantType') === false
            || $client->supportsGrantType($grantType) === true;
    }

    /**
     * Gets the client credentials from the request from the request body or
     * the Http Basic Authorization header
     *

View on GitHub (pinned to 9d2f6fc0a0)