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
- Add the requested grant type (e.g. 'client_credentials', 'refresh_token') to the client's allowed grants in your client store.
- Check your ClientRepository/ClientEntity grant-type logic (supportsGrantType source) and make sure the grant identifier from $grant->getIdentifier() is included.
- Use a separate client record with the correct grant types for service-to-service (client_credentials) usage instead of reusing the web app client.
- 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
- Model clients with an explicit grant_types column/list and enforce it at registration time.
- Use distinct client records per integration pattern (web app vs service).
- Add integration tests per grant type per client.
- Review client grant lists when upgrading the library.
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
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)