{"record":{"id":"6765904ac2c4312b","repo":"phacility/phabricator","slug":"access-token-error-s","errorCode":null,"errorMessage":"Access token error: %s","messagePattern":"Access token error: (.+?)","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/auth/adapter/PhutilOAuthAuthAdapter.php","lineNumber":193,"sourceCode":"    $data = $this->readAccessTokenResponse($body);\n\n    if (isset($data['expires_in'])) {\n      $data['expires_epoch'] = $data['expires_in'];\n    } else if (isset($data['expires'])) {\n      $data['expires_epoch'] = $data['expires'];\n    }\n\n    // If we got some \"expires\" value back, interpret it as an epoch timestamp\n    // if it's after the year 2010 and as a relative number of seconds\n    // otherwise.\n    if (isset($data['expires_epoch'])) {\n      if ($data['expires_epoch'] < (60 * 60 * 24 * 365 * 40)) {\n        $data['expires_epoch'] += time();\n      }\n    }\n\n    if (isset($data['error'])) {\n      throw new Exception(pht('Access token error: %s', $data['error']));\n    }\n\n    return $data;\n  }\n\n  protected function readAccessTokenResponse($body) {\n    // NOTE: Most providers either return JSON or HTTP query strings, so try\n    // both mechanisms. If your provider does something else, override this\n    // method.\n\n    $data = json_decode($body, true);\n\n    if (!is_array($data)) {\n      $data = array();\n      parse_str($body, $data);\n    }\n\n    if (empty($data['access_token']) &&","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/auth/adapter/PhutilOAuthAuthAdapter.php#L175-L211","documentation":"In the OAuth 2 token exchange (makeTokenRequest()), after POSTing code/client_id/client_secret/redirect_uri to the token endpoint, the decoded body (JSON or query string) is checked for an error key. Per RFC 6749 section 5.2 providers report failures like invalid_grant, invalid_client, or redirect_uri_mismatch in that field, and this exception re-throws the provider's error string verbatim.","triggerScenarios":"Exchanging an authorization code that is expired, already used, or issued for a different client; a wrong client_secret (invalid_client); a redirect_uri that does not exactly match the one used in the authorize step (invalid_grant/redirect_uri_mismatch); provider app in a suspended/unapproved state.","commonSituations":"User takes too long on the consent page and the code expires; the browser back-button causes a code to be exchanged twice; secret rotated or copy-pasted with whitespace; app configured with a different callback domain; provider enforcing exact redirect_uri matching while trailing slashes differ.","solutions":["Restart the authentication flow from the beginning so a fresh authorization code is issued (codes are single-use and short-lived).","Verify the client secret stored for the provider matches the provider's app settings exactly.","Compare the redirect_uri sent during token exchange with the one used in the authorize request - they must match byte-for-byte, including trailing slashes and scheme.","Check the provider's app configuration for suspended status, missing review approval, or wrong scopes."],"exampleFix":"// before: a stale or already-consumed code fails the exchange\ntry {\n  $token = $adapter->getAccessToken();\n} catch (Exception $ex) {\n  die($ex->getMessage()); // 'Access token error: invalid_grant'\n}\n\n// after: on exchange failure, discard the code and restart the flow for a fresh one\ntry {\n  $token = $adapter->getAccessToken();\n} catch (Exception $ex) {\n  return $this->restartHandshake(); // re-issue the authorize redirect\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  $token_data = $adapter->getAccessTokenData();\n} catch (Exception $ex) {\n  if (preg_match('/Access token error: (invalid_grant|expired_code)/', $ex->getMessage())) {\n    // Code was consumed or expired: discard it and restart the flow fresh.\n    return $this->restartHandshake();\n  }\n  // invalid_client / redirect_uri mismatch are config bugs: surface them.\n  throw $ex;\n}","preventionTips":["Never exchange an authorization code more than once; treat 'back button' re-submits as flow restarts.","Store client secrets exactly as issued (no surrounding whitespace) and rotate them when exposed.","Keep the redirect_uri byte-identical between the authorize and token steps."],"tags":["oauth2","authentication","token-exchange","third-party-provider","php"],"backgroundTag":"oauth2-token-exchange-failed","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}