Skip to content

Auth/PM-3813 - 2FA Management Endpoints - User Verification Refactor#7841

Draft
JaredSnider-Bitwarden wants to merge 9 commits into
mainfrom
auth/pm-38137/2fa-verification-refactor
Draft

Auth/PM-3813 - 2FA Management Endpoints - User Verification Refactor#7841
JaredSnider-Bitwarden wants to merge 9 commits into
mainfrom
auth/pm-38137/2fa-verification-refactor

Conversation

@JaredSnider-Bitwarden

@JaredSnider-Bitwarden JaredSnider-Bitwarden commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-38137

📔 Objective

To refactor our 2FA management endpoints to use our tokenable framework for ongoing user verification (previously proven by its adoption for authenticator). The GET endpoints require normal user verification via secret which then mints a user verification tokenable which can be presented to the PUT or DELETE endpoints for changing the data.

📸 Screenshots

See clients PR: TODO

Introduces a new tokenable bound to UserId + ProviderType for the upcoming
per-provider 2FA flow, alongside its factory, DI registration, and configurable
lifetime. Pure addition with no behavior change to existing flows.

- TwoFactorUserVerificationTokenable + ITwoFactorUserVerificationTokenableFactory + factory implementation
- Unit tests for tokenable and factory
- IGlobalSettings.TwoFactorUserVerificationTokenLifetimeInMinutes (default 30)
- DI registration for the data protector + tokenable factory
Per-provider disable request models for the upcoming DELETE endpoints, a
wrapper response model for the WebAuthn challenge, and a UserVerificationToken
field on the existing response models and PUT request models that will carry
the new replay token. Pure addition — no existing wire contracts narrow here.
…endpoints

Replace CheckAsync with three single-purpose helpers (ValidateUserBySecretAsync,
ValidateUserVerificationTokenAsync, ValidateUserHasPremiumAsync) plus a
MintProtectedUserVerificationToken helper. Each call site composes the guards
it needs explicitly.

Other changes in this refactor:
- New per-provider DELETE endpoints: DisableYubiKey, DisableDuo, DisableEmail,
  DisableOrganizationDuo
- GetWebAuthnChallenge returns the new TwoFactorWebAuthnChallengeResponseModel
  wrapper so a UV token can travel with the FIDO2 options
- DisableAuthenticator hardcodes TwoFactorProviderType.Authenticator
- GetYubiKey and GetDuo no longer gate on premium; lapsed-premium users can
  read their own configuration and use the standard GET -> DELETE flow
- Legacy PutDisable and PutOrganizationDisable endpoints removed; per-provider
  DELETEs replace them
- Inline Task.Delay calls dropped from rewritten methods; rate limiting belongs
  at the edge
- Unit test coverage extended: Goal-7 non-premium GET assertions, per-endpoint
  validator negative paths through PutDuo, organization NotFound branches for
  both PutOrganizationDuo and DisableOrganizationDuo, and the DisableOrganizationDuo
  happy path
The Authenticator PUT and DELETE request models inherited fields they no
longer read (Secret, MasterPasswordHash, Type). Rewrite both as standalone
classes carrying only the fields the controller actually uses. With those
two models no longer inheriting it, TwoFactorProviderRequestModel has no
remaining consumers and is removed.

- UpdateTwoFactorAuthenticatorRequestModel: standalone with Token, Key,
  UserVerificationToken
- TwoFactorAuthenticatorDisableRequestModel: standalone with UserVerificationToken,
  Key
- TwoFactorProviderRequestModel: deleted
- Integration tests updated to stop referencing the dropped fields
Drop the unused third parameter from the VerifySecretAsync signature and
inline the remaining conditional returns. Existing callers all pass the
two-argument form so no downstream changes are required; the existing
VerifySecretAsync_Works theory continues to cover password and OTP paths.
Adds end-to-end coverage for the GET, PUT, and DELETE paths on every 2FA
provider:

- Per-provider GET round-trip tests that mint via the controller and replay
  the resulting token against the matching DELETE (or PUT for the WebAuthn
  challenge endpoint), verifying the token round-trip survives DI + wire
  serialization
- Per-provider PUT happy paths exercising the token-replay chain end-to-end
- SendEmail invokes the email service when given a valid token
- DisableAuthenticator_BodyTypeMismatch_RespectsUrlRoute confirms the URL is
  the sole source of provider truth and any Type field in the body is dropped
  by deserialization
- DisableYubiKey_CrossProviderToken_BadRequest confirms the validator's
  provider-type binding rejects cross-provider replay
@JaredSnider-Bitwarden JaredSnider-Bitwarden changed the title Auth/pm 38137/2fa verification refactor Auth/PM-3813 - 2FA Management Endpoints - User Verification Refactor Jun 19, 2026
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.77419% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.56%. Comparing base (2706c74) to head (aaf3631).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/Api/Auth/Controllers/TwoFactorController.cs 96.96% 2 Missing and 1 partial ⚠️
.../Api/Auth/Models/Request/TwoFactorRequestModels.cs 77.77% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7841      +/-   ##
==========================================
+ Coverage   61.25%   61.56%   +0.30%     
==========================================
  Files        2193     2217      +24     
  Lines       97296    97823     +527     
  Branches     8767     8819      +52     
==========================================
+ Hits        59601    60220     +619     
+ Misses      35582    35454     -128     
- Partials     2113     2149      +36     

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

@JaredSnider-Bitwarden JaredSnider-Bitwarden added the ai-review Request a Claude code review label Jun 19, 2026
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the 2FA management endpoint refactor that moves per-provider GET → PUT/DELETE flows onto the TwoFactorUserVerificationTokenable framework, mints provider-bound user-verification tokens, removes the legacy disable endpoints, and simplifies VerifySecretAsync. Token binding (user + provider type), expiry handling, premium gating, and organization Duo authorization all check out, and the unit + integration test coverage is thorough (cross-provider replay, expired tokens, wrong user, non-premium read paths).

Code Review Details

No new findings. One pre-existing unresolved thread remains:

  • ❓ : Task.Delay(2000) brute-force/timing delay removal on failed secret/token verification
    • src/Api/Auth/Controllers/TwoFactorController.cs:544

Notes considered and not flagged:

  • The isSettingMFA/skipVerification removal closes the temporary PM-9925 bypass so GET endpoints now perform real secret verification — verified all other VerifySecretAsync callers used the default and are unaffected.
  • The GetWebAuthnChallenge response shape change (CredentialCreateOptions → envelope) is a coordinated breaking change with the clients PR.
  • The "single-use" wording on TwoFactorUserVerificationTokenable is aspirational (tokens are replayable within the 30-min lifetime), consistent with the existing authenticator tokenable; not a behavior bug.

Per-provider DELETE /two-factor/webauthn/all that mirrors the legacy
generic disable behavior for WebAuthn. The existing per-credential
DELETE /two-factor/webauthn refuses to remove the last registered
credential by design, so a bulk path is required for users who want
to disable WebAuthn entirely. Unit and integration coverage included.
…points

Mirror the five-test pattern (valid token, expired token, TryUnprotect
failure, token bound to different user, token bound to different provider)
across DisableYubiKey, DisableDuo, and DisableEmail. Brings the
non-Authenticator per-provider DELETE endpoints up to the same unit-test
shape recently added for DisableWebAuthnAll.
Comment thread src/Api/Auth/Controllers/TwoFactorController.cs
Per-provider 2FA endpoints reached via HTTP DELETE were named DisableX on
the controller. The underlying behavior is a hard removal of the provider
configuration, not a reversible disable, so the method and request-model
names now read as DeleteX to match what the code actually does.

Scope is limited to the controller surface and its request models. The
underlying service methods (IUserService / IOrganizationService) keep
their historical DisableTwoFactorProviderAsync names but gain XML doc
comments clarifying that they hard-delete the provider configuration.
@sonarqubecloud

Copy link
Copy Markdown

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant