RocketChat/Rocket.Chat · error · Error

Failed to complete OAuth handshake with ${this.name} at ${th

Error message

Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.error}

What it means

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.

Source

Thrown at apps/meteor/server/lib/auth-providers/custom-oauth/custom_oauth_server.js:167

				ignoreSsrfValidation: true,
				method: 'POST',
				headers,
				body: params,
			});

			if (!request.ok) {
				throw new Error(request.statusText);
			}

			response = await request.json();
		} catch (err) {
			const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`);
			throw _.extend(error, { response: err.response });
		}

		if (response.error) {
			// if the http response was a json object with an error attribute
			throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.error}`);
		} else {
			return response;
		}
	}

	async getIdentity(accessToken) {
		const params = {};
		const headers = {
			'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user
			'Accept': 'application/json',
		};

		if (this.identityTokenSentVia === 'header') {
			headers.Authorization = `Bearer ${accessToken}`;
		} else {
			params[this.accessTokenParam] = accessToken;
		}

View on GitHub (pinned to b2c16d5842)

Solutions

  1. 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
  2. Make the redirect URI registered at the provider exactly match `${Site_Url}/_oauth/<name>`
  3. Set tokenSentVia ('header' or 'payload') to match how the provider expects client credentials on the token request
  4. Confirm tokenPath is the real token endpoint (default '/oauth/token'; some providers use /oauth2/token or /login/oauth/access_token)

Example fix

// before: stale secret + wrong tokenSentVia
new CustomOAuth('gitlab', { serverURL: 'https://gitlab.example', tokenSentVia: 'header' });
// -> Failed to complete OAuth handshake with gitlab at https://gitlab.example/oauth/token. invalid_client

// after: rotate secret in Admin -> OAuth -> gitlab, keep credentials in the payload (GitLab default),
// and register redirect URI https://chat.example.com/_oauth/gitlab on the provider
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: catch config mistakes before users do
const preflightTokenEndpoint = async (tokenPath: string) => {
  const res = await fetch(tokenPath, {
    method: 'POST',
    body: new URLSearchParams({ grant_type: 'authorization_code' }),
  });
  if (res.ok) return;
  const body = await res.json().catch(() => ({}));
  throw new Error(`token endpoint preflight failed: ${body.error ?? res.status}`);
};

Try / catch

try {
  const token = await customOAuth.getAccessToken(query);
} catch (error) {
  const msg = String(error.message);
  if (/invalid_client/.test(msg)) return fail('Client id/secret wrong - re-save OAuth credentials');
  if (/invalid_grant/.test(msg)) return fail('Code expired, reused, or redirect_uri mismatch');
  if (/ECONNREFUSED|ENOTFOUND|certificate/i.test(msg)) return fail('Network/TLS problem reaching the IdP');
  return fail(msg);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/5d45142d4d113fda. Report an issue: GitHub.