RocketChat / RocketChat/Rocket.Chat
SAML login broken in RC 8.4.1
Nobody has claimed this yet.
- 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
- Configure SAML login (any IdP — reproduced with Keycloak)
- On a mobile device, open the Rocket.Chat React Native app
- Tap the SAML login button → complete authentication at the IdP
- The in-app webview redirects back to the Rocket.Chat server and completes login successfully
- The native app attempts
POST /api/v1/loginwith the same credential token → 401 - 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
masteranddevelop
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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