Skip to content

[PM-40673] Two Factor Remember Token Updates (Part 1) - #8427

Merged
rr-bw merged 27 commits into
mainfrom
auth/pm-40673/part-1
Oct 21, 2026
Merged

rr-bw merged 27 commits into
mainfrom
auth/pm-40673/part-1

Conversation

@rr-bw

@rr-bw rr-bw commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

🎟️ Tracking

PM-40673

PR Stack

  • auth/pm-40673/part-1 - Entity, Repository, and Command/Query updates
  • auth/pm-40673/part-2 - Add token issuance to login flow (and migrates from old-to-new tokens)
  • auth/pm-40673/part-3 - Updates call sites where we want to revoke 2FA remember tokens (admin 2FA reset, recovery code usage, removal of last 2FA method)

📔 Objective

Commit 1 - 239ad75

  • Adds a new C# entity (TwoFactorRememberToken) with a rotatable Stamp.
  • Adds a new repository interface (ITwoFactorRememberTokenRepository), defining methods that act on TwoFactorRememberToken row(s):
    • Point lookup by (UserId, DeviceId)
    • Upsert
    • Bulk Stamp rotation
    • Bulk delete

Commit 2 - 6d5f8ee

  • Lays the MSSQL foundation:
    • src/Sql/ Table schema and View
    • Migrator script
    • Stored procedures (ReadByUserIdDeviceId, Save, DeleteExpired, UpdateManyStampsByUserId)
    • Dapper repository implementation and DI wiring

Commit 3 - d463a2f

  • Adds Entity Framework infrastructure
    • TwoFactorRememberTokenEntityTypeConfiguration.cs
    • EF repository implementation
    • Generated migration files

Commit 4 - 939a391

  • Adds TwoFactorRememberTokenable.cs

Commit 5 - 48aec51

  • Adds commands for issuing and revoking tokens, and a query for validating tokens

Commit 6 - 9130de8

  • Adds a job for deleting expired tokens.

📸 Screenshots

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.82759% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.55%. Comparing base (e6b10b5) to head (523502c).

Files with missing lines Patch % Lines
...h/Repositories/TwoFactorRememberTokenRepository.cs 88.31% 6 Missing and 3 partials ⚠️
src/Admin/Jobs/JobsHostedService.cs 14.28% 6 Missing ⚠️
...h/Repositories/TwoFactorRememberTokenRepository.cs 96.51% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8427      +/-   ##
==========================================
+ Coverage   67.12%   73.55%   +6.42%     
==========================================
  Files        2674     2685      +11     
  Lines      114478   114826     +348     
  Branches    10390    10420      +30     
==========================================
+ Hits        76847    84458    +7611     
+ Misses      35122    27742    -7380     
- Partials     2509     2626     +117     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 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.

@rr-bw
rr-bw added this pull request to stack #8466 September 28, 2026 15:59
@rr-bw
rr-bw marked this pull request as ready for review September 28, 2026 16:36
@rr-bw
rr-bw requested review from a team as code owners September 28, 2026 16:36
@rr-bw
rr-bw requested a review from enmande September 28, 2026 16:36
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-reviewed at 523502cbf. Since the last review, the DeleteExpiredTwoFactorRememberTokensJob trigger moved to Saturday 04:30 (Eastern, or local time when self-hosted), which no longer collides with DatabaseExpiredGrantsJob or DeleteUnverifiedOrganizationDomainsJob. ValidateTwoFactorRememberTokenQuery no longer checks whether 2FA is enabled or takes an organization parameter; it now validates only the token, the user binding, the security stamp, the device identifier, the row stamp and the row expiry, and the interface remarks say the 2FA-required decision belongs to the caller. The rest of the change set (entity, Dapper and EF repositories with their duplicate-key handling, the SSDT schema and the 2026-10-17_00 migration, the EF migrations and snapshots, the tokenable and its factory, the issue and revoke commands, and the DI wiring) was checked again, and no new issues were found.

@mkincaid-bw mkincaid-bw 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.

Overall a good PR from the DB side, with a couple minor changes requested.

@@ -0,0 +1,57 @@
CREATE PROCEDURE [dbo].[TwoFactorRememberToken_Save]

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.

This type of Upsert is not Bitwarden's preferred methodology of performing an Insert/Update. This should be broken into a TwoFactorRememberToken_Create proc and a TwoFactorRememberToken_Update proc. See docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@@ -0,0 +1,22 @@
CREATE PROCEDURE [dbo].[TwoFactorRememberToken_DeleteExpired]

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.

⛏️ This should probably be named TwoFactorRememberToken_DeleteManyExpired

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

-- Deleted in batches so each statement holds few enough locks to avoid escalating to a table
-- lock, which would block logins while the sweep runs. The loop ends when a batch deletes
-- nothing.
DECLARE @BatchSize INT = 100

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.

A batch size of 100 is the norm throughout the codebase. However, if you expect this proc to thousands of rows (or more) during each run, then @BatchSize should be raised to 1000.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@rr-bw rr-bw added ai-review Request a Claude code review t:feature Change Type - Feature Development labels Oct 2, 2026
@rr-bw
rr-bw requested review from enmande and mkincaid-bw October 5, 2026 15:31
@rr-bw rr-bw removed the ai-review Request a Claude code review label Oct 6, 2026
mkincaid-bw
mkincaid-bw previously approved these changes Oct 6, 2026

@mkincaid-bw mkincaid-bw 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.

LGTM

@rr-bw rr-bw added the ai-review Request a Claude code review label Oct 9, 2026
mkincaid-bw
mkincaid-bw previously approved these changes Oct 12, 2026

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

Awesome! Thank you ✨
Two questions below: the migration date ordering, and the job trigger timing.
Just LMK on those, everything else looks great.

Comment thread src/Admin/Jobs/JobsHostedService.cs Resolved
@rr-bw rr-bw removed the ai-review Request a Claude code review label Oct 17, 2026
@rr-bw
rr-bw force-pushed the auth/pm-40673/part-1 branch from 6a7c424 to 6d27724 Compare October 19, 2026 14:12
rr-bw added 22 commits October 21, 2026 14:48
…Job to run at a new time to avoid multiple jobs running at same time
… parameter from ValidateTwoFactorRememberTokenQuery
@rr-bw
rr-bw force-pushed the auth/pm-40673/part-1 branch from 6d27724 to 523502c Compare October 21, 2026 19:48
SET NOCOUNT ON

SELECT
*

@aikido-pr-checks aikido-pr-checks Bot Oct 21, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The TwoFactorRememberToken_ReadByUserIdDeviceId procedure selects *, making its returned columns fragile to view schema changes.

Show fix
Suggested change
*
[Id],
[UserId],
[DeviceId],
[Stamp],
[CreationDate],
[RevisionDate],
[ExpirationDate]
Details

✨ AI Reasoning
​The query uses an explicit wildcard, so its returned columns can change when the view schema changes and its data dependencies are obscured.

Reply on this comment to give feedback or ignore the issue.
More info

CREATE VIEW [dbo].[TwoFactorRememberTokenView]
AS
SELECT
*

@aikido-pr-checks aikido-pr-checks Bot Oct 21, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The TwoFactorRememberTokenView selects *, so its exposed columns change implicitly when the underlying table schema changes.

Show fix
Suggested change
*
[Id],
[UserId],
[DeviceId],
[Stamp],
[CreationDate],
[RevisionDate],
[ExpirationDate]
Details

✨ AI Reasoning
​The view exposes every table column through an explicit wildcard, coupling its output to future table schema changes.

Reply on this comment to give feedback or ignore the issue.
More info

CREATE OR ALTER VIEW [dbo].[TwoFactorRememberTokenView]
AS
SELECT
*

@aikido-pr-checks aikido-pr-checks Bot Oct 21, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The migration's TwoFactorRememberTokenView selects *, so its exposed columns change implicitly when the underlying table schema changes.

Show fix
Suggested change
*
[Id],
[UserId],
[DeviceId],
[Stamp],
[CreationDate],
[RevisionDate],
[ExpirationDate]
Details

✨ AI Reasoning
​The migration defines a view with an explicit wildcard, making the view's exposed columns depend implicitly on the table schema.

Reply on this comment to give feedback or ignore the issue.
More info

Comment on lines +104 to +105
Microsoft.Data.Sqlite.SqliteException lite => lite.SqliteErrorCode == 19
&& lite.SqliteExtendedErrorCode is 1555 or 2067,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The lite exception filter mixes && with an or pattern on one line, making the duplicate-key condition harder to parse.

Show fix
Suggested change
Microsoft.Data.Sqlite.SqliteException lite => lite.SqliteErrorCode == 19
&& lite.SqliteExtendedErrorCode is 1555 or 2067,
Microsoft.Data.Sqlite.SqliteException lite =>
lite.SqliteErrorCode == 19 &&
lite.SqliteExtendedErrorCode is 1555 or 2067,
Details

✨ AI Reasoning
​This condition combines a SQLite error-code check with an alternative pattern match. The mixed operators make it harder to scan the exact conditions under which the exception is treated as a duplicate-key violation.

Reply on this comment to give feedback or ignore the issue.
More info

@rr-bw
rr-bw merged commit ae47d2b into main Oct 21, 2026
77 checks passed
@rr-bw
rr-bw deleted the auth/pm-40673/part-1 branch October 21, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants