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

  1. Verify the exact same redirect_uri value is sent to the token endpoint as was used in the authorize request
  2. Ensure each authorization code is redeemed exactly once — generate a fresh code via the authorize endpoint for every token exchange
  3. Confirm client_id/client_secret at the token endpoint match the client the code was issued to
  4. Exchange the code promptly; if it may be stale, restart the authorization flow
  5. 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

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)