thephpleague/oauth2-server · error · OAuthServerException
invalid_grant
invalid_grant
Error message
Cannot validate the provided authorization code
What it means
The authorization code could not be validated during the access token exchange. In respondToAccessTokenRequest, the decrypted auth code payload is passed to validateAuthorizationCode (client match, expiry, user binding, scope finalization); any InvalidArgumentException from that validation is swallowed and rethrown as a generic invalid_grant error with this message. This is the standard OAuth2 invalid_grant outcome — the code exists syntactically but fails one of the authorization-code validity checks.
Solutions
- Verify the exact same redirect_uri value is sent to the token endpoint as was used in the authorize request
- Ensure each authorization code is redeemed exactly once — generate a fresh code via the authorize endpoint for every token exchange
- Confirm client_id/client_secret at the token endpoint match the client the code was issued to
- Exchange the code promptly; if it may be stale, restart the authorization flow
- Check server clock synchronization if codes appear to expire immediately
Example fix
// before tokenParams.redirect_uri = 'https://app.example.com/callback/'; // after tokenParams.redirect_uri = authorizeParams.redirect_uri; // must byte-match the authorize request
Defensive patterns
Strategy: try-catch
Validate before calling
if (!code || typeof code !== 'string') throw new Error('authorization code required before token exchange'); Try / catch
try { $tokens = $grant->respondToAccessTokenRequest($request, $responseType, $ttl); } catch (OAuthServerException $e) { if ($e->getErrorType() === 'invalid_grant') { // code expired/used/mismatched: redirect user to re-authorize } throw $e; } Prevention
- Redeem each authorization code exactly once
- Byte-match redirect_uri between authorize and token requests
- Exchange codes immediately after receipt
- Keep client credentials consistent per environment
When it happens
Trigger: POSTing to the token endpoint with a `code` that (a) was issued to a different client_id, (b) has expired, (c) has already been redeemed/revoked, (d) was issued for a different redirect_uri than the one sent, or (e) whose user/scopes fail validateAuthorizationCode or finalizeScopes with an InvalidArgumentException.
Common situations: Client sends the code twice (e.g. double-submit or retry after a timeout — codes are single-use and revoked on first exchange); redirect_uri differs between authorize and token calls (trailing slash, http vs https, changed domain); clock skew or long delay between authorize and token steps past the code TTL (default 10 min); swapping client credentials between environments.
Related errors
AI-assisted analysis of thephpleague/oauth2-server@9d2f6fc0a0 (2026-09-15).
Data as JSON: /api/errors/2e477565b2fc127a.
Report an issue: GitHub.
Appendix: source
Thrown at src/Grant/AuthCodeGrant.php:121
if ($encryptedAuthCode === null) {
throw OAuthServerException::invalidRequest('code');
}
try {
$authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
$this->validateAuthorizationCode($authCodePayload, $client, $request);
$scopes = $this->scopeRepository->finalizeScopes(
$this->validateScopes($authCodePayload->scopes),
$this->getIdentifier(),
$client,
$authCodePayload->user_id,
$authCodePayload->auth_code_id
);
} catch (InvalidArgumentException $e) {
throw OAuthServerException::invalidGrant('Cannot validate the provided authorization code');
} catch (LogicException $e) {
throw OAuthServerException::invalidRequest('code', 'Issue decrypting the authorization code', $e);
}
$codeVerifier = $this->getRequestParameter('code_verifier', $request);
// If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack
if (!isset($authCodePayload->code_challenge) && $codeVerifier !== null) {
throw OAuthServerException::invalidRequest(
'code_challenge',
'code_verifier received when no code_challenge is present'
);
}
if (isset($authCodePayload->code_challenge)) {
$this->validateCodeChallenge($authCodePayload, $codeVerifier);
}
View on GitHub (pinned to 9d2f6fc0a0)