RocketChat / RocketChat/Rocket.Chat

SAML login broken in RC 8.4.1

Open
#40,605 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

type: bug
Dominant language
TypeScript
Stars
46.1k
Forks
13.9k
Avg merge
3d 3h
Merged PRs (30d)
130

Description

Description

SAML login via the React Native mobile app fails with HTTP 401 "No matching login attempt found" approximately 95–99% of the time. The root cause is a race condition in loginHandler.ts: the credential token is consumed (deleted from MongoDB) on first redemption, but the mobile login flow requires the same token to be redeemed by two parallel consumers — the in-app webview and the native app itself.

When the webview wins the race (the common case), the native app's subsequent POST /api/v1/login finds no matching token and fails. Patching the server to defer token removal makes mobile SAML logins succeed reliably.

Steps to Reproduce

  1. Configure SAML login (any IdP — reproduced with Keycloak)
  2. On a mobile device, open the Rocket.Chat React Native app
  3. Tap the SAML login button → complete authentication at the IdP
  4. The in-app webview redirects back to the Rocket.Chat server and completes login successfully
  5. The native app attempts POST /api/v1/login with the same credential token → 401
  6. The app remains on the login screen despite the webview having logged in

Expected Behavior

The native app should successfully redeem the SAML credential token and transition to the logged-in state.

Actual Behavior

POST /api/v1/login returns 401 with {"status":"error","error":"No matching login attempt found"}. Mobile SAML login fails ~95–99% of attempts. The remaining 1–5% of successes occur only when the native app's request happens to reach the server before the webview's — i.e., when the native app wins the race.

Root Cause Analysis

Token flow (verified via mitmproxy on iOS)
# Request Token Originator
1 GET /_saml/authorize/<provider>/<A> Ty8DeNDhyRmOM9BrW App generates token A
2 SAML AuthnRequest to IdP, ID="A" Ty8DeNDhyRmOM9BrW Server propagates
3 IdP POST /_saml/validate/<provider> with InResponseTo="A" Signed assertion from IdP
4 Server-issued redirect: GET /saml/<B>?saml_idp_credentialToken=<B> ry6GLrGT6Zg2iJZWe Server-issued token B
5 Webview: POST /api/v1/method.callAnon/login with credentialToken=B ry6GLrGT6Zg2iJZWe Webview wins race → 200, token removed
6 Native app: POST /api/v1/login with credentialToken=B ry6GLrGT6Zg2iJZWe App → 401 (token gone)
Code location

apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts (line 25):

const loginResult = await SAML.retrieveCredential(loginRequest.credentialToken);
await CredentialTokens.removeById(loginRequest.credentialToken);   // ← token is single-use

The token is removed unconditionally after the first redemption. Both the webview (via method.callAnon/login) and the native app (via /api/v1/login) invoke this same handler with the same token. Whichever request arrives first wins; the other gets 401.

This bug is present in master and develop branches as of the time of this report.

Confirmation via patch

Commenting out the removeById call (and verifying the patch is active via log output) makes mobile SAML logins succeed reliably. The webview and native app can both redeem the same token, as required by the mobile login flow.

The MongoDB collection rocketchat_credential_tokens already has a TTL index on expireAt with expireAfterSeconds: 0, so leaving the token in place is not a leak — it will be cleaned up automatically by the TTL job once it expires.

Environment

  • Rocket.Chat server: 8.4.1 (Docker, official image)
  • Identity Provider: Keycloak (irrelevant — race is server-internal)
  • Mobile app: Rocket.Chat React Native iOS v4.72.0, v4.73.0 (both reproduce)
  • Code inspection: bug also present in current master and develop

Suggested Fix

Defer token removal so the mobile webview and native app can both redeem the same token within a short grace period. The TTL index already in place on expireAt handles eventual cleanup.

Minimal patch:

const loginResult = await SAML.retrieveCredential(loginRequest.credentialToken);
// Don't remove the token immediately — the mobile app's webview and native
// client both attempt to redeem the same token concurrently. Rely on the
// TTL index on `expireAt` for cleanup.
// await CredentialTokens.removeById(loginRequest.credentialToken);

Alternative (explicit deferred removal if a grace window is preferred over relying on TTL):

setTimeout(() => {
    void CredentialTokens.removeById(loginRequest.credentialToken).catch((err) => {
        SystemLogger.warn({ msg: 'Failed to remove credential token', err });
    });
}, 30000);
Security consideration

The patch widens the token replay window from 0 seconds to the TTL duration (or the chosen grace period). Given that:

  • The token has ~17 characters of entropy
  • It is transmitted only over HTTPS
  • The TTL is short (seconds, not minutes)
  • The mobile flow legitimately requires multiple redemptions

…the trade-off is favorable: a small theoretical replay window in exchange for a working mobile login that currently fails for the vast majority of users.

Related

  • PR #14345 (2019): "[FIX] SAML credentialToken removal was preventing mobile from being able to authenticate" — addressed a similar issue at the time, suggesting this category of bug has surfaced repeatedly.
  • Issue #30049: related token-handling regression.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts and trace the shared handler used by method.callAnon/login and POST /api/v1/login. Review CredentialTokens expiry and its MongoDB TTL index before choosing a bounded redemption approach. Done means both mobile consumers can redeem the credential token reliably without leaving tokens beyond their intended expiry, with the reported 401 no longer occurring.

Written by the indexing model from the issue text.

Assessment

Tech stack
mongodb, react-native, typescript
Domain
api, authentication, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.