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
- 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)
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
- 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
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- could-not-access-webdav
- could-not-access-webdav
- field_not_found
- CustomOAuth: Failed to extract username
- CustomOAuth: Failed to extract email
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/5d45142d4d113fda.
Report an issue: GitHub.