{"record":{"id":"2e477565b2fc127a","repo":"thephpleague/oauth2-server","slug":"invalid-grant","errorCode":"invalid_grant","errorMessage":"Cannot validate the provided authorization code","messagePattern":"Cannot validate the provided authorization code","errorType":"http","errorClass":"OAuthServerException","httpStatus":400,"severity":"error","filePath":"src/Grant/AuthCodeGrant.php","lineNumber":121,"sourceCode":"\n        if ($encryptedAuthCode === null) {\n            throw OAuthServerException::invalidRequest('code');\n        }\n\n        try {\n            $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));\n\n            $this->validateAuthorizationCode($authCodePayload, $client, $request);\n\n            $scopes = $this->scopeRepository->finalizeScopes(\n                $this->validateScopes($authCodePayload->scopes),\n                $this->getIdentifier(),\n                $client,\n                $authCodePayload->user_id,\n                $authCodePayload->auth_code_id\n            );\n        } catch (InvalidArgumentException $e) {\n            throw OAuthServerException::invalidGrant('Cannot validate the provided authorization code');\n        } catch (LogicException $e) {\n            throw OAuthServerException::invalidRequest('code', 'Issue decrypting the authorization code', $e);\n        }\n\n        $codeVerifier = $this->getRequestParameter('code_verifier', $request);\n\n        // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack\n        if (!isset($authCodePayload->code_challenge) && $codeVerifier !== null) {\n            throw OAuthServerException::invalidRequest(\n                'code_challenge',\n                'code_verifier received when no code_challenge is present'\n            );\n        }\n\n        if (isset($authCodePayload->code_challenge)) {\n            $this->validateCodeChallenge($authCodePayload, $codeVerifier);\n        }\n","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/thephpleague/oauth2-server/blob/9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c/src/Grant/AuthCodeGrant.php#L103-L139","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ntokenParams.redirect_uri = 'https://app.example.com/callback/';\n\n// after\ntokenParams.redirect_uri = authorizeParams.redirect_uri; // must byte-match the authorize request","handlingStrategy":"try-catch","validationCode":"if (!code || typeof code !== 'string') throw new Error('authorization code required before token exchange');","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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"],"tags":["oauth2","invalid-grant","authorization-code","php-league-oauth2-server"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"9d2f6fc0a0b5aa1bb02506971d3a4ecff2c6526c","analyzedAt":"2026-09-15T22:33:30.452Z","contentChangedAt":"2026-09-15T22:33:30.452Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}