phacility/phabricator · error · Exception
Access token error: %s
Error message
Access token error: %s
What it means
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.
Source
Thrown at src/applications/auth/adapter/PhutilOAuthAuthAdapter.php:193
$data = $this->readAccessTokenResponse($body);
if (isset($data['expires_in'])) {
$data['expires_epoch'] = $data['expires_in'];
} else if (isset($data['expires'])) {
$data['expires_epoch'] = $data['expires'];
}
// If we got some "expires" value back, interpret it as an epoch timestamp
// if it's after the year 2010 and as a relative number of seconds
// otherwise.
if (isset($data['expires_epoch'])) {
if ($data['expires_epoch'] < (60 * 60 * 24 * 365 * 40)) {
$data['expires_epoch'] += time();
}
}
if (isset($data['error'])) {
throw new Exception(pht('Access token error: %s', $data['error']));
}
return $data;
}
protected function readAccessTokenResponse($body) {
// NOTE: Most providers either return JSON or HTTP query strings, so try
// both mechanisms. If your provider does something else, override this
// method.
$data = json_decode($body, true);
if (!is_array($data)) {
$data = array();
parse_str($body, $data);
}
if (empty($data['access_token']) &&View on GitHub (pinned to 5720a38cfe)
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.
Example fix
// before: a stale or already-consumed code fails the exchange
try {
$token = $adapter->getAccessToken();
} catch (Exception $ex) {
die($ex->getMessage()); // 'Access token error: invalid_grant'
}
// after: on exchange failure, discard the code and restart the flow for a fresh one
try {
$token = $adapter->getAccessToken();
} catch (Exception $ex) {
return $this->restartHandshake(); // re-issue the authorize redirect
} Defensive patterns
Strategy: retry
Try / catch
try {
$token_data = $adapter->getAccessTokenData();
} catch (Exception $ex) {
if (preg_match('/Access token error: (invalid_grant|expired_code)/', $ex->getMessage())) {
// Code was consumed or expired: discard it and restart the flow fresh.
return $this->restartHandshake();
}
// invalid_client / redirect_uri mismatch are config bugs: surface them.
throw $ex;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to decode OAuth access token response: %s
- Expected '%s' to be '%s'!
- Expected token to finish OAuth handshake!
- Expected verifier to finish OAuth handshake!
- Expected '%s' in response!
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/6765904ac2c4312b.
Report an issue: GitHub.