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

  1. Restart the authentication flow from the beginning so a fresh authorization code is issued (codes are single-use and short-lived).
  2. Verify the client secret stored for the provider matches the provider's app settings exactly.
  3. 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.
  4. 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

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


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/6765904ac2c4312b. Report an issue: GitHub.