Skip to content

feat(auth): enforce account disabled state on every sign-in path - #21257

Merged
toufali merged 7 commits into
mainfrom
fxa-14470-disabled-account-enforcement
Sep 23, 2026
Merged

toufali merged 7 commits into
mainfrom
fxa-14470-disabled-account-enforcement

Conversation

@toufali

@toufali toufali commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Because

  • Disabling an account blocks only the email and password login form.

This pull request

  • Refuses to create a session, token or unblock code for a disabled account inside lib/db.ts, where every sign-in and reset path ends.
  • Keeps an early check on password login so a disabled account cannot be used to test password guesses.
  • Adds sessionWithDevice_20 so session-token lookups carry disabledAt, and rejects those tokens in every session strategy.
  • Checks the account on every /oauth/token grant type.
  • Adds revokeAccountTokens_1; admin disable now revokes sessions, devices, auth tokens, OAuth refresh tokens and codes.
  • Records the account.disable and account.enable security events server-side.
  • Refuses to re-create a disabled account through the unverified-account signup path.
  • Adds errno 240 ACCOUNT_DISABLED, replacing errno 149 on the password login paths.

Issue that this pull request solves

Closes: FXA-14470
Closes: FXA-14462

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

Start with the token-creating functions in lib/db.ts, then makeCredentialFn in lib/server.js, the migration, then the admin-server controller.

Manual test on a local stack (yarn start):

  1. Create an account at http://localhost:3030 and add a passkey in Settings.
  2. Sign in to the same account in a second browser profile and leave it open.
  3. In the admin panel (http://localhost:8091), find the account and click Disable.
  4. Reload the second profile: it is signed out.
  5. Sign in with the password, then with the passkey: both fail with "This account has been disabled".
  6. Request a password reset and enter the emailed code: the code step fails with the same error.
  7. In the admin panel, the account shows account.disable in security events; click Enable.
  8. Sign in with the password: it works, and account.enable is recorded.

Other information (Optional)

  • Deploy ordering: apply patch 201 before auth-server and admin-server roll.
  • The admin-server MySQL user needs EXECUTE on revokeAccountTokens_1.
  • Already-issued OAuth access tokens are not revoked; they expire within six hours by default.
  • Accounts disabled before this change are locked out on deploy, but their stale token rows are not cleaned up.
  • errno 240 is also returned on pre-authentication routes, and content-server has no string for it.
  • Follow-up: drop sessionWithDevice_19 in a later patch.

Because:

- Disabling an account only blocked the email and password login form.
  Passkey, third-party and passwordless sign-in, password reset, and
  existing sessions and OAuth tokens all kept working.
- The disabled state is the containment action for fraud and abuse work,
  so it has to hold on every path.

This commit:

- Adds a shared assertAccountEnabled check to every route that signs in,
  resets, or mints a credential for an account.
- Joins accounts.disabledAt into the session-token lookup so every token
  strategy rejects a disabled account's sessions.
- Checks the account on every OAuth token grant.
- Makes admin disable revoke sessions, tokens and OAuth grants, and record
  the security event server-side.
- Adds errno 240 ACCOUNT_DISABLED with the fxa-settings message.

Closes FXA-14470
Closes FXA-14462
Because:

- The Build job type-checks what the unit suites transpile without
  checking, and two of the new call sites did not compile.

This commit:

- Adds disabledAt to the passkey route's account type so the shared
  check accepts it.
- Widens the DangerZone test props so disabledAt can hold a timestamp.
Because:

- The remote /token spec builds its own auth-db stub, which lacked the
  method the grant handler now calls.

This commit:

- Adds an accountDisabledAt stub resolving to null.

Copilot AI 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.

🟡 Changes recommended

OAuth issuance and disable retries still allow disabled-account credentials to survive or be minted.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Enforces disabled-account state across authentication, credential, password-reset, session, OAuth, and administration flows.

Changes:

  • Adds shared account-enabled checks and errno 240.
  • Revokes account sessions, devices, and OAuth credentials on disable.
  • Moves security-event recording server-side and updates admin UI messaging.
File summaries
File Description
packages/fxa-shared/test/db/models/auth/sp_revokeAccountTokens.sql Adds revocation procedure fixture.
packages/fxa-shared/test/db/models/auth/index.spec.ts Tests auth-token revocation.
packages/fxa-shared/test/db/models/auth/helpers.ts Loads the procedure fixture.
packages/fxa-shared/db/models/auth/session-token.ts Exposes disabled state on sessions.
packages/fxa-shared/db/models/auth/base-auth.ts Registers new procedures.
packages/fxa-shared/db/models/auth/account.ts Adds account token revocation.
packages/fxa-settings/src/lib/auth-errors/en.ftl Localizes disabled-account errors.
packages/fxa-settings/src/lib/auth-errors/auth-errors.ts Maps the new error.
packages/fxa-auth-server/test/remote/oauth_token_route.in.spec.ts Updates OAuth database mocks.
packages/fxa-auth-server/test/mocks.js Adds disabled-state mock support.
packages/fxa-auth-server/lib/tokens/session_token.spec.ts Tests disabled-state propagation.
packages/fxa-auth-server/lib/tokens/session_token.js Stores disabled state on tokens.
packages/fxa-auth-server/lib/server.js Rejects disabled-account sessions.
packages/fxa-auth-server/lib/server.in.spec.ts Tests session rejection and metrics.
packages/fxa-auth-server/lib/routes/utils/signin.spec.ts Tests disabled password sign-in.
packages/fxa-auth-server/lib/routes/utils/signin.js Guards shared sign-in logic.
packages/fxa-auth-server/lib/routes/utils/account.ts Adds shared account-state guards.
packages/fxa-auth-server/lib/routes/utils/account.spec.ts Tests account-state guards.
packages/fxa-auth-server/lib/routes/unblock-codes.spec.ts Tests unblock-code rejection.
packages/fxa-auth-server/lib/routes/unblock-codes.js Guards unblock-code creation.
packages/fxa-auth-server/lib/routes/session.spec.ts Updates duplicated-session shape tests.
packages/fxa-auth-server/lib/routes/session.js Documents disabled login errors.
packages/fxa-auth-server/lib/routes/passwordless.ts Guards passwordless authentication.
packages/fxa-auth-server/lib/routes/passwordless.spec.ts Tests passwordless rejection.
packages/fxa-auth-server/lib/routes/password.ts Guards password changes and resets.
packages/fxa-auth-server/lib/routes/password.spec.ts Tests disabled password flows.
packages/fxa-auth-server/lib/routes/passkeys.ts Guards passkey sign-in.
packages/fxa-auth-server/lib/routes/passkeys.spec.ts Tests disabled passkey sign-in.
packages/fxa-auth-server/lib/routes/oauth/token.spec.ts Tests disabled OAuth grants.
packages/fxa-auth-server/lib/routes/oauth/token.js Checks account state before token issuance.
packages/fxa-auth-server/lib/routes/linked-accounts.ts Guards linked-account sign-in.
packages/fxa-auth-server/lib/routes/linked-accounts.spec.ts Tests linked-account rejection.
packages/fxa-auth-server/lib/routes/emails.spec.ts Tests verification rejection.
packages/fxa-auth-server/lib/routes/emails.js Guards recovery-email verification.
packages/fxa-auth-server/lib/routes/account.ts Guards account, reset, and key flows.
packages/fxa-auth-server/lib/routes/account.spec.ts Tests guarded account flows.
packages/fxa-auth-server/lib/db.ts Adds disabled-state lookup.
packages/fxa-auth-server/lib/db.spec.ts Tests disabled-state lookup.
packages/fxa-admin-server/src/rest/account/account.controller.ts Implements disable/enable orchestration.
packages/fxa-admin-server/src/rest/account/account.controller.spec.ts Tests administrative transitions.
packages/fxa-admin-server/src/database/database.service.ts Revokes auth and OAuth credentials.
packages/fxa-admin-panel/src/components/PageAccountSearch/DangerZone/index.tsx Updates administrative controls.
packages/fxa-admin-panel/src/components/PageAccountSearch/DangerZone/index.test.tsx Tests disable/enable UI flows.
packages/db-migrations/databases/fxa/target-patch.json Advances schema patch level.
packages/db-migrations/databases/fxa/patches/patch-201-200.sql Adds rollback instructions.
packages/db-migrations/databases/fxa/patches/patch-200-201.sql Adds session and revocation procedures.
libs/accounts/errors/src/constants.ts Defines errno 240.
libs/accounts/errors/src/app-error.ts Defines the disabled-account error.
Review details
  • Files reviewed: 48/48 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/fxa-admin-server/src/rest/account/account.controller.ts
Comment thread packages/fxa-auth-server/lib/routes/oauth/token.js
Comment thread packages/fxa-admin-server/src/rest/account/account.controller.ts Outdated
Comment thread packages/fxa-admin-server/src/database/database.service.ts
Because:

- Refusing to re-disable an already-disabled account meant a failed
  revocation could never be retried without enabling the account first.

This commit:

- Drops the disabledAt IS NULL condition from the disable update.

Copilot AI 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.

🟡 Changes recommended

Token-issuance races and failed revocation can leave credentials usable or reactivate them when an account is enabled.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

packages/fxa-shared/test/db/models/auth/index.spec.ts:575

  • This matrix covers only part of the new procedure. There are no fixtures or assertions for accountResetTokens, passwordChangeTokens, passwordForgotTokens, or the joined deviceCommands delete, so regressions can leave high-value credentials active while this suite passes. Add populated fixtures for each deletion target and verify target rows are removed without affecting another UID.

packages/fxa-admin-server/src/database/database.service.ts:154

  • The controller tests mock this method, while the existing DatabaseService integration suite does not exercise it, so the new direct OAuth deletes are unverified. Add an integration test that inserts codes and refresh tokens for both the target and another UID, invokes this method, and verifies only the target rows are removed.
    await this.account.revokeTokens(uid);
    await this.knexOauth('refreshTokens').where('userId', uidBuffer).del();
    await this.knexOauth('codes').where('userId', uidBuffer).del();
  • Files reviewed: 48/48 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread packages/fxa-admin-server/src/rest/account/account.controller.ts
Comment thread packages/fxa-auth-server/lib/routes/oauth/token.js
Because:

- Flagging first meant a failed revocation left the account disabled
  with live credentials, which a later enable would reactivate.

This commit:

- Runs revokeAccountTokens before the disabledAt update so a failure
  leaves the account enabled and the action retryable.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Credential-revocation races and unguarded recovery-token routes can bypass the intended disabled-account guarantees.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 3 High severity

Open (3)
Resolved since last review (5)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Missing client mapping for account-disabled error

libs/​accounts/​errors/​src/​constants.ts:148

Errno 240 is now returned from legacy password login/reset paths, but fxa-content-server/app/scripts/lib/auth-errors.js has no matching entry. That client therefore displays the raw English server message instead of a localized account-disabled error. Add the corresponding content-server error mapping and localization before replacing errno 149 on those paths.

Low severity Integration fixture omits four token/device tables

packages/​fxa-shared/​test/​db/​models/​auth/​index.spec.ts:577

The integration fixture exercises only five of the procedure's eight token/device tables. No row is inserted for accountResetTokens, passwordChangeTokens, passwordForgotTokens, or deviceCommands, so regressions in those new DELETE statements would still leave this suite green. Seed and assert each deleted table, including preservation of another account's rows.

Comment thread packages/fxa-admin-server/src/rest/account/account.controller.ts
Comment thread packages/fxa-auth-server/lib/routes/utils/account.ts
Comment thread packages/fxa-auth-server/lib/server.js
@toufali
toufali requested a balanced review from Copilot September 21, 2026 18:08
@toufali
toufali marked this pull request as ready for review September 21, 2026 18:14
@toufali
toufali requested review from a team as code owners September 21, 2026 18:14

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Disable and enable state changes can commit without their required security events or profile notifications when event insertion fails.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (3)

Comment thread packages/fxa-admin-server/src/rest/account/account.controller.ts Outdated
Because:

- A failed security-event write returned an error after the account
  state had already changed, and skipped the profile notification.

This commit:

- Records the security event, clears the profile cache and notifies
  attached services independently, reporting any failure to Sentry.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

The security-critical revocation integration test omits several token tables and device-command cleanup paths.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Expand integration test coverage for all procedure deletion clauses

packages/​fxa-shared/​test/​db/​models/​auth/​index.spec.ts:575

This integration test does not exercise all rows the new procedure promises to revoke. It never seeds or asserts accountResetTokens, passwordChangeTokens, passwordForgotTokens, or the joined deviceCommands, so regressions in four deletion clauses would still pass. Seed those tables for the target and another UID, then verify target removal and preservation of unrelated rows.

@dschom dschom 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.

I was expecting to see this happen at the authorization strategy layer. The approach you have here seems hard to maintain and touches a lot of files!

…e place

Because:

- Checking each sign-in route separately touched nine files and left
  future routes to remember the rule themselves.

This commit:

- Refuses to create any session, token or unblock code for a disabled
  account inside the db layer, where every sign-in and reset path ends.
- Removes the per-route checks and their route-level tests.
- Keeps the early password-login check and the OAuth grant check.

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

The revocation integration test omits several security-sensitive credential tables deleted by the new procedure.

Review effort: Balanced
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Low severity Add integration coverage for deleting all security token tables

packages/​fxa-shared/​test/​db/​models/​auth/​index.spec.ts:577

The integration coverage omits accountResetTokens, passwordChangeTokens, passwordForgotTokens, and deviceCommands, even though the new procedure is responsible for deleting all four. A missing or broken DELETE for any of these security-sensitive credentials would still pass this suite; seed each table and include it in the post-revocation assertions.

@toufali

toufali commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

I was expecting to see this happen at the authorization strategy layer. The approach you have here seems hard to maintain and touches a lot of files!

Good idea @dschom - sign-in check now moved to the db functions that create sessions and tokens, so every path hits it and the per-route checks are gone.

Note, a disabled user now sees the error after entering their OTP code rather than before. Should be fine for a disabled account.

@dschom dschom 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.

Thanks for the updates. LGTM :shipit:

@toufali
toufali merged commit bb898bb into main Sep 23, 2026
22 checks passed
@toufali
toufali deleted the fxa-14470-disabled-account-enforcement branch September 23, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants