{"record":{"id":"3bb29f06e2e16389","repo":"phacility/phabricator","slug":"expected-token-to-finish-oauth-handshake","errorCode":null,"errorMessage":"Expected token to finish OAuth handshake!","messagePattern":"Expected token to finish OAuth handshake!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/auth/adapter/PhutilOAuth1AuthAdapter.php","lineNumber":166,"sourceCode":"    $confirmed = idx($data, 'oauth_callback_confirmed');\n    if ($confirmed !== 'true') {\n      throw new Exception(\n        pht(\"Expected '%s' to be '%s'!\", 'oauth_callback_confirmed', 'true'));\n    }\n\n    $this->readTokenAndTokenSecret($data);\n\n    $authorize_token_uri = new PhutilURI($this->getAuthorizeTokenURI());\n    $authorize_token_uri->replaceQueryParam('oauth_token', $this->getToken());\n\n    return phutil_string_cast($authorize_token_uri);\n  }\n\n  protected function finishOAuthHandshake() {\n    $this->willFinishOAuthHandshake();\n\n    if (!$this->getToken()) {\n      throw new Exception(pht('Expected token to finish OAuth handshake!'));\n    }\n    if (!$this->getVerifier()) {\n      throw new Exception(pht('Expected verifier to finish OAuth handshake!'));\n    }\n\n    $validate_uri = $this->getValidateTokenURI();\n    $params = array(\n      'oauth_verifier' => $this->getVerifier(),\n    );\n\n    list($body) = $this->newOAuth1Future($validate_uri, $params)->resolvex();\n    $data = id(new PhutilQueryStringParser())->parseQueryString($body);\n\n    $this->readTokenAndTokenSecret($data);\n\n    $this->handshakeData = $data;\n  }\n","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/auth/adapter/PhutilOAuth1AuthAdapter.php#L148-L184","documentation":"finishOAuthHandshake() refuses to run when the adapter has no request token stored. The OAuth 1.0a flow is two-phase: getAuthenticateURI() first obtains and stores a request token (via readTokenAndTokenSecret), the user is redirected, and only then can the handshake be finished with that token plus the verifier. This exception means the second phase was entered without the first phase having happened in this adapter instance.","triggerScenarios":"Calling finishOAuthHandshake() (directly or through an adapter's getAccountData()/login path) on a freshly constructed adapter whose setToken() was never called or was called with an empty value; a controller that reaches the callback branch of the flow before getAuthenticateURI() ever ran; state loss between the redirect request and the callback request because adapters are rebuilt per request and the token was not persisted/restored.","commonSituations":"Custom login controller invoking the callback logic directly; a broken flow order after refactoring; the oauth_token query parameter from the provider's redirect being dropped before it is fed back into the adapter; session/state storage silently failing.","solutions":["Ensure getAuthenticateURI() is called first and the request token it stores is persisted (e.g., in the auth workflow state) across the redirect.","On the callback request, restore the token into the adapter with setToken() before triggering the handshake completion.","Check that the provider's oauth_token redirect parameter is actually reaching your code and being applied.","Verify you are not constructing a brand-new adapter instance on the callback and expecting its token to survive."],"exampleFix":"// before: finish phase runs on an adapter that never got a request token\n$adapter = $provider->getAdapter();\n$adapter->finishOAuthHandshake();\n\n// after: restore the request token captured during the authenticate phase\n$adapter = $provider->getAdapter();\n$adapter->setToken($stored_request_token);\n$adapter->finishOAuthHandshake();","handlingStrategy":"validation","validationCode":"// Restore request-token state before finishing the handshake\nif (!strlen($adapter->getToken())) {\n  $stored_token = $auth_workflow_state->getRequestToken();\n  if (!strlen($stored_token)) {\n    return $this->restartAuthenticateFlow();\n  }\n  $adapter->setToken($stored_token);\n}","typeGuard":null,"tryCatchPattern":"try {\n  $adapter->finishOAuthHandshake();\n} catch (Exception $ex) {\n  // Token state was lost; send the user back through getAuthenticateURI().\n  return $this->restartAuthenticateFlow();\n}","preventionTips":["Persist the request token in the auth workflow state between the redirect and the callback.","Never call the finish phase on an adapter you did not just restore state into.","Treat a missing token as 'restart flow', not as a hard failure."],"tags":["oauth1","authentication","handshake","state-management","php"],"backgroundTag":"oauth1-handshake-failed","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}