{"record":{"id":"5d45142d4d113fda","repo":"RocketChat/Rocket.Chat","slug":"failed-to-complete-oauth-handshake-with-this-nam","errorCode":null,"errorMessage":"Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.error}","messagePattern":"Failed to complete OAuth handshake with (.+?) at (.+?)\\. (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/lib/auth-providers/custom-oauth/custom_oauth_server.js","lineNumber":167,"sourceCode":"\t\t\t\tignoreSsrfValidation: true,\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders,\n\t\t\t\tbody: params,\n\t\t\t});\n\n\t\t\tif (!request.ok) {\n\t\t\t\tthrow new Error(request.statusText);\n\t\t\t}\n\n\t\t\tresponse = await request.json();\n\t\t} catch (err) {\n\t\t\tconst error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`);\n\t\t\tthrow _.extend(error, { response: err.response });\n\t\t}\n\n\t\tif (response.error) {\n\t\t\t// if the http response was a json object with an error attribute\n\t\t\tthrow new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.error}`);\n\t\t} else {\n\t\t\treturn response;\n\t\t}\n\t}\n\n\tasync getIdentity(accessToken) {\n\t\tconst params = {};\n\t\tconst headers = {\n\t\t\t'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user\n\t\t\t'Accept': 'application/json',\n\t\t};\n\n\t\tif (this.identityTokenSentVia === 'header') {\n\t\t\theaders.Authorization = `Bearer ${accessToken}`;\n\t\t} else {\n\t\t\tparams[this.accessTokenParam] = accessToken;\n\t\t}\n","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/auth-providers/custom-oauth/custom_oauth_server.js#L149-L185","documentation":"During the authorization-code exchange, the token endpoint (serverURL + tokenPath) returned a JSON body containing an 'error' attribute (this branch); the sibling catch one block above wraps pure transport failures with a nearly identical message. The message embeds the service name, the exact token URL, and the provider's error string, so the provider's own OAuth2 error code (invalid_client, invalid_grant, invalid_request, ...) is the real diagnosis.","triggerScenarios":"POST to the token URL replies {\"error\":\"invalid_client\"} because the client secret is wrong or credentials were sent in the wrong place (tokenSentVia 'header' vs default payload); {\"error\":\"invalid_grant\"} because the authorization code expired, was reused, or redirect_uri differs from the registered callback `${Site_Url}/_oauth/<name>`; clock skew between Rocket.Chat and the provider shortening code validity; tokenPath pointing at the wrong endpoint.","commonSituations":"Secret rotated on the provider but not in Rocket.Chat admin; Site_Url changed (http->https, domain move) so redirect_uri no longer matches; provider expects HTTP Basic auth but tokenSentVia left as 'payload'; provider API version bump moving the token endpoint path.","solutions":["Read the provider error string in the message: invalid_client -> re-copy Client id/Secret in Admin -> OAuth and re-save; invalid_grant -> check code expiry, NTP clock sync, and redirect_uri equality","Make the redirect URI registered at the provider exactly match `${Site_Url}/_oauth/<name>`","Set tokenSentVia ('header' or 'payload') to match how the provider expects client credentials on the token request","Confirm tokenPath is the real token endpoint (default '/oauth/token'; some providers use /oauth2/token or /login/oauth/access_token)"],"exampleFix":"// before: stale secret + wrong tokenSentVia\nnew CustomOAuth('gitlab', { serverURL: 'https://gitlab.example', tokenSentVia: 'header' });\n// -> Failed to complete OAuth handshake with gitlab at https://gitlab.example/oauth/token. invalid_client\n\n// after: rotate secret in Admin -> OAuth -> gitlab, keep credentials in the payload (GitLab default),\n// and register redirect URI https://chat.example.com/_oauth/gitlab on the provider","handlingStrategy":"try-catch","validationCode":"// preflight: catch config mistakes before users do\nconst preflightTokenEndpoint = async (tokenPath: string) => {\n  const res = await fetch(tokenPath, {\n    method: 'POST',\n    body: new URLSearchParams({ grant_type: 'authorization_code' }),\n  });\n  if (res.ok) return;\n  const body = await res.json().catch(() => ({}));\n  throw new Error(`token endpoint preflight failed: ${body.error ?? res.status}`);\n};","typeGuard":null,"tryCatchPattern":"try {\n  const token = await customOAuth.getAccessToken(query);\n} catch (error) {\n  const msg = String(error.message);\n  if (/invalid_client/.test(msg)) return fail('Client id/secret wrong - re-save OAuth credentials');\n  if (/invalid_grant/.test(msg)) return fail('Code expired, reused, or redirect_uri mismatch');\n  if (/ECONNREFUSED|ENOTFOUND|certificate/i.test(msg)) return fail('Network/TLS problem reaching the IdP');\n  return fail(msg);\n}","preventionTips":["Rotate provider secrets and Rocket.Chat OAuth settings together","Freeze Site_Url - every change silently invalidates registered redirect URIs","Sync server clocks with NTP; authorization codes are short-lived","Always log the full error string - the provider's error code beats guessing"],"tags":["oauth","token-exchange","http","custom-oauth","network"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}