thephpleague/oauth2-server · error · OAuthServerException
invalid request: client_secret
Error message
invalid request: client_secret
What it means
Validation helper (validateClient) for confidential clients: the client identified by client_id exists and is confidential, so a client_secret is mandatory, but the request supplied an empty secret (or none at all). The generic invalidRequest error is raised with the 'client_secret' parameter name to signal that the missing credential — not the id — is the fault in this grant request.
Solutions
- Include client_secret in the token request body: client_id=...&client_secret=...&grant_type=...
- If the client is genuinely public, register it as public (isConfidential() false) so no secret is required.
- If using HTTP Basic auth, ensure it's enabled/expected; otherwise put the secret in the body.
- Check the deployed environment actually has a non-empty CLIENT_SECRET value.
Example fix
// before POST /token grant_type=password&client_id=web&username=u&password=p // after POST /token grant_type=password&client_id=web&client_secret=SECRET&username=u&password=p
Defensive patterns
Strategy: validation
Validate before calling
if ($client->isConfidential() && ($_POST['client_secret'] ?? '') === '') {
throw new \InvalidArgumentException('client_secret is required for confidential clients');
} Type guard
function hasClientSecret(array $body): bool { return isset($body['client_secret']) && is_string($body['client_secret']) && $body['client_secret'] !== ''; } Try / catch
try { $token = $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $e) { return $e->generateHttpResponse($response); } Prevention
- Register SPA/mobile clients as public if they cannot keep a secret
- Load CLIENT_SECRET from env and fail fast at boot if empty
- Send credentials as form-encoded body unless the server is configured for HTTP Basic
When it happens
Trigger: POST /token with grant_type=password or authorization_code from a confidential client without the client_secret field in the (form-encoded) body; secret passed in header but server expects body, or empty string sent.
Common situations: Public SPA treating its confidential backend client as public; HTTP Basic credentials not parsed (server expects body params); empty env var for the secret deployed to production.
Related errors
- invalid request: response_type
- unsupported grant type
- Missing "Authorization" header
- Missing "Bearer" token
- Access token could not be verified
AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15).
Data as JSON: /api/errors/0ffc6584c8bf187c.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/AbstractGrant.php:158
public function revokeRefreshTokens(bool $willRevoke): void
{
$this->revokeRefreshTokens = $willRevoke;
}
/**
* 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 contractView on GitHub (pinned to 9d2f6fc0a0)