Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dee8b0c
PM-38137 - Per-provider 2FA request and response models on client
JaredSnider-Bitwarden Jun 19, 2026
0bd18c6
PM-38137 - Expose per-provider delete methods on 2FA service layer
JaredSnider-Bitwarden Jun 19, 2026
1c0100e
PM-38137 - Thread UV token through web 2FA setup components
JaredSnider-Bitwarden Jun 19, 2026
3b52026
PM-38137 - Preserve lapsed-premium 2FA disable shortcut on settings list
JaredSnider-Bitwarden Jun 22, 2026
bdf2c1c
PM-38137 - Add TODO to get rid of base 2FA setup component.
JaredSnider-Bitwarden Jun 22, 2026
144d34b
PM-38137 - Rename 2FA update request models to TwoFactor<Provider>Upd…
JaredSnider-Bitwarden Jun 23, 2026
e6c1c6b
PM-38137 - Split Email 2FA request models by flow
JaredSnider-Bitwarden Jun 23, 2026
47736fb
PM-38137 - Guard cached UV token against null overwrite on PUT response
JaredSnider-Bitwarden Jun 23, 2026
1129bf4
PM-38137 - Refactor 2FA request DTOs to constructor parameters
JaredSnider-Bitwarden Jun 23, 2026
29408bd
PM-38137 - Add 2FA provider Details and per-action response classes
JaredSnider-Bitwarden Jun 24, 2026
fe157b9
PM-38137 - Extract WebAuthn challenge response class and rename for c…
JaredSnider-Bitwarden Jun 24, 2026
000ece6
PM-38137 - Refactor 2FA service surface and components to per-action …
JaredSnider-Bitwarden Jun 24, 2026
7062529
PM-38137 - Move 2FA request/response models into two-factor feature f…
JaredSnider-Bitwarden Jun 24, 2026
d710e36
PM-38137 - Align authenticator delete request naming with sibling DTOs
JaredSnider-Bitwarden Jun 24, 2026
c5a628d
PM-38137 - Move 2FA type aliases into two-factor feature folder with …
JaredSnider-Bitwarden Jun 24, 2026
cf079a6
PM-38137 - Correct 2FA dialog-data type docs to match constructor-par…
JaredSnider-Bitwarden Jun 24, 2026
f60578f
PM-38137 - Split TwoFactorUserVerificationResult into its own file
JaredSnider-Bitwarden Jun 24, 2026
44251e2
Merge remote-tracking branch 'origin/main' into auth/pm-38137/2fa-ver…
JaredSnider-Bitwarden Jun 24, 2026
9b56f34
PM-38137 - Convert two-factor API service imports to relative paths
JaredSnider-Bitwarden Jun 24, 2026
85724e0
PM-38137 - Drop !: on cached userVerificationToken in 2FA setup compo…
JaredSnider-Bitwarden Jun 24, 2026
daabc4f
PM-38137 - Rename applyXxxState param to details for clarity
JaredSnider-Bitwarden Jun 24, 2026
20ea54c
PM-38137 - 2FA Setup comp base - add TODO
JaredSnider-Bitwarden Jun 24, 2026
6fdab96
PM-38137 - Prefix applyXxxState param with provider name
JaredSnider-Bitwarden Jun 24, 2026
c08eb46
PM-38137 - Rename applyXxxState to applyXxxDetails in 2FA setup compo…
JaredSnider-Bitwarden Jun 25, 2026
71e4b64
PM-38137 - Run prettier on 2FA setup/verify components
JaredSnider-Bitwarden Jun 25, 2026
ef05dbc
PM-38137 - Reword 2FA delete doc strings to drop stale disable verbiage
JaredSnider-Bitwarden Jun 25, 2026
db712e8
PM-38137 - Inline processUpdate/processDeleteResponse pass-throughs
JaredSnider-Bitwarden Jun 25, 2026
c4db105
PM-38137 - Replace endpoint-fragile 2FA response docstrings with sing…
JaredSnider-Bitwarden Jun 25, 2026
82b0592
Merge branch 'main' into auth/pm-38137/2fa-verification-refactor
JaredSnider-Bitwarden Jun 25, 2026
279e72b
PM-38137 - Replay UV token on get-webauthn-challenge client call
JaredSnider-Bitwarden Jun 25, 2026
a8eba06
PM-38137 - Guard cached UV token in 2FA authenticator setup component
JaredSnider-Bitwarden Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/cli/src/auth/commands/login.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import {
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorService, TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorEmailLoginRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-email-login.request";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
Expand Down Expand Up @@ -300,7 +300,7 @@ export class LoginCommand {
}

if (twoFactorToken == null && selectedProvider.type === TwoFactorProviderType.Email) {
const emailReq = new TwoFactorEmailRequest();
const emailReq = new TwoFactorEmailLoginRequest();
emailReq.email = await this.loginStrategyService.getEmail();
// if the user was logging in with SSO, we need to include the SSO session token
if (response.ssoEmail2FaSessionToken != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { TwoFactorService, TwoFactorSetupDialogData } from "@bitwarden/common/auth/two-factor";
import { TwoFactorOrganizationDuoResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-organization-duo.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
Expand Down Expand Up @@ -93,9 +92,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
data: { type: type, organizationId: this.organizationId },
});
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
twoFactorVerifyDialogRef.closed,
);
const result: TwoFactorSetupDialogData<TwoFactorOrganizationDuoResponse> =
await lastValueFrom(twoFactorVerifyDialogRef.closed);
if (!result) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
import { WebAuthnChallengeResponse } from "@bitwarden/common/auth/models/response/web-authn-challenge.response";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";

/**
* Options provided by the server to be used during attestation (i.e. creation of a new webauthn credential)
*/
export class WebauthnLoginCredentialCreateOptionsResponse extends BaseResponse {
/** Options to be provided to the webauthn authenticator */
options: ChallengeResponse;
options: WebAuthnChallengeResponse;

/**
* Contains an encrypted version of the {@link options}.
Expand All @@ -16,7 +16,7 @@ export class WebauthnLoginCredentialCreateOptionsResponse extends BaseResponse {

constructor(response: unknown) {
super(response);
this.options = new ChallengeResponse(this.getResponseProperty("options"));
this.options = new WebAuthnChallengeResponse(this.getResponseProperty("options"));
this.token = this.getResponseProperty("token");
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChallengeResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
import { WebAuthnChallengeResponse } from "@bitwarden/common/auth/models/response/web-authn-challenge.response";

export class CredentialCreateOptionsView {
constructor(
readonly options: ChallengeResponse,
readonly options: WebAuthnChallengeResponse,
readonly token: string,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { firstValueFrom } from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
import { ChangeEmailService } from "@bitwarden/common/auth/services/change-email/change-email.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-provider.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";

import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-recover.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
ButtonModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { TwoFactorService, TwoFactorSetupDialogData } from "@bitwarden/common/auth/two-factor";
import { TwoFactorAuthenticatorDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-authenticator-delete.request";
import { TwoFactorAuthenticatorUpdateRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-authenticator-update.request";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-authenticator.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
Expand Down Expand Up @@ -85,7 +84,14 @@ export class TwoFactorSetupAuthenticatorComponent
@Output() onChangeStatus = new EventEmitter<boolean>();
type = TwoFactorProviderType.Authenticator;
key: string;
private userVerificationToken: string;
private userVerificationToken: string | undefined;

private requireUserVerificationToken(): string {
if (this.userVerificationToken === undefined) {
throw new Error("User verification token is missing");
}
return this.userVerificationToken;
}

override componentName = "app-two-factor-authenticator";
qrScriptError = false;
Expand All @@ -96,7 +102,7 @@ export class TwoFactorSetupAuthenticatorComponent
});

constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorAuthenticatorResponse>,
@Inject(DIALOG_DATA) protected data: TwoFactorSetupDialogData<TwoFactorAuthenticatorResponse>,
private dialogRef: DialogRef,
twoFactorService: TwoFactorService,
i18nService: I18nService,
Expand Down Expand Up @@ -136,9 +142,9 @@ export class TwoFactorSetupAuthenticatorComponent
this.formGroup.controls.token.markAsTouched();
}

async auth(authResponse: AuthResponse<TwoFactorAuthenticatorResponse>) {
async auth(authResponse: TwoFactorSetupDialogData<TwoFactorAuthenticatorResponse>) {
super.auth(authResponse);
return this.processResponse(authResponse.response);
return this.processGetResponse(authResponse.response);
}

submit = async () => {
Expand All @@ -155,13 +161,17 @@ export class TwoFactorSetupAuthenticatorComponent
};

protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
request.token = this.formGroup.value.token;
request.key = this.key;
request.userVerificationToken = this.userVerificationToken;
const request = new TwoFactorAuthenticatorUpdateRequest(
this.formGroup.value.token,
this.key,
this.requireUserVerificationToken(),
);

const response = await this.twoFactorService.putTwoFactorAuthenticator(request);
await this.processResponse(response);
await this.applyAuthenticatorDetails(
response.authenticator.enabled,
response.authenticator.key,
);
this.onUpdated.emit(true);
}

Expand All @@ -176,10 +186,10 @@ export class TwoFactorSetupAuthenticatorComponent
return;
}

const request = await this.buildRequestModel(DisableTwoFactorAuthenticatorRequest);
request.type = this.type;
request.key = this.key;
request.userVerificationToken = this.userVerificationToken;
const request = new TwoFactorAuthenticatorDeleteRequest(
this.key,
this.requireUserVerificationToken(),
);
await this.twoFactorService.deleteTwoFactorAuthenticator(request);
this.enabled = false;
this.toastService.showToast({
Expand All @@ -190,11 +200,18 @@ export class TwoFactorSetupAuthenticatorComponent
this.onUpdated.emit(false);
}

private async processResponse(response: TwoFactorAuthenticatorResponse) {
this.formGroup.get("token").setValue(null);
this.enabled = response.enabled;
this.key = response.key;
private async processGetResponse(response: TwoFactorAuthenticatorResponse) {
this.userVerificationToken = response.userVerificationToken;
await this.applyAuthenticatorDetails(
response.authenticator.enabled,
response.authenticator.key,
);
}

private async applyAuthenticatorDetails(enabled: boolean, key: string) {
this.formGroup.get("token").setValue(null);
this.enabled = enabled;
this.key = key;

await this.waitForQRiousToLoadOrError().catch((error) => {
this.logService.error(error);
Expand Down Expand Up @@ -238,7 +255,7 @@ export class TwoFactorSetupAuthenticatorComponent

static open(
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorAuthenticatorResponse>>,
config: DialogConfig<TwoFactorSetupDialogData<TwoFactorAuthenticatorResponse>>,
) {
return dialogService.open<boolean>(TwoFactorSetupAuthenticatorComponent, config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";

import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { TwoFactorService, TwoFactorSetupDialogData } from "@bitwarden/common/auth/two-factor";
import { TwoFactorDuoDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-duo-delete.request";
import { TwoFactorDuoUpdateRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-duo-update.request";
import { TwoFactorOrganizationDuoDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-organization-duo-delete.request";
import { TwoFactorDuoDetailsResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-duo-details.response";
import { TwoFactorDuoUpdateResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-duo-update.response";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-duo.response";
import { TwoFactorOrganizationDuoUpdateResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-organization-duo-update.response";
import { TwoFactorOrganizationDuoResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-organization-duo.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
Expand All @@ -30,6 +35,11 @@ import { I18nPipe } from "@bitwarden/ui-common";

import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";

type TwoFactorDuoResponseUnion = TwoFactorDuoResponse | TwoFactorOrganizationDuoResponse;
type TwoFactorDuoUpdateResponseUnion =
| TwoFactorDuoUpdateResponse
| TwoFactorOrganizationDuoUpdateResponse;

// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
Expand Down Expand Up @@ -64,6 +74,14 @@ export class TwoFactorSetupDuoComponent
host: ["", [Validators.required]],
});
override componentName = "app-two-factor-duo";
private userVerificationToken: string | undefined;

private requireUserVerificationToken(): string {
if (this.userVerificationToken === undefined) {
throw new Error("User verification token is missing");
}
return this.userVerificationToken;
}

constructor(
@Inject(DIALOG_DATA) protected data: TwoFactorDuoComponentConfig,
Expand Down Expand Up @@ -113,7 +131,7 @@ export class TwoFactorSetupDuoComponent
}

super.auth(this.data.authResponse);
this.processResponse(this.data.authResponse.response);
this.processGetResponse(this.data.authResponse.response);

if (this.data.organizationId) {
this.type = TwoFactorProviderType.OrganizationDuo;
Expand All @@ -135,12 +153,14 @@ export class TwoFactorSetupDuoComponent
};

protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.clientId = this.clientId;
request.clientSecret = this.clientSecret;
request.host = this.host;
const request = new TwoFactorDuoUpdateRequest(
this.clientId,
this.clientSecret,
this.host,
this.requireUserVerificationToken(),
);

let response: TwoFactorDuoResponse;
let response: TwoFactorDuoUpdateResponseUnion;

if (this.organizationId != null) {
response = await this.twoFactorService.putTwoFactorOrganizationDuo(
Expand All @@ -151,19 +171,54 @@ export class TwoFactorSetupDuoComponent
response = await this.twoFactorService.putTwoFactorDuo(request);
}

this.processResponse(response);
this.applyDuoDetails(response.duo);
this.onUpdated.emit(true);
}

protected override async disableMethod() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "disable" },
content: { key: "twoStepDisableDesc" },
type: "warning",
});

if (!confirmed) {
return;
}

if (this.organizationId != null) {
const request = new TwoFactorOrganizationDuoDeleteRequest(
this.requireUserVerificationToken(),
);
await this.twoFactorService.deleteTwoFactorOrganizationDuo(this.organizationId, request);
} else {
const request = new TwoFactorDuoDeleteRequest(this.requireUserVerificationToken());
await this.twoFactorService.deleteTwoFactorDuo(request);
}

this.enabled = false;
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("twoStepDisabled"),
});
this.onUpdated.emit(false);
}

onClose = () => {
void this.dialogRef.close(this.enabled);
};

private processResponse(response: TwoFactorDuoResponse) {
this.clientId = response.clientId;
this.clientSecret = response.clientSecret;
this.host = response.host;
this.enabled = response.enabled;
private processGetResponse(response: TwoFactorDuoResponseUnion) {
this.userVerificationToken = response.userVerificationToken;
this.applyDuoDetails(response.duo);
}

private applyDuoDetails(duoDetails: TwoFactorDuoDetailsResponse) {
this.clientId = duoDetails.clientId;
this.clientSecret = duoDetails.clientSecret;
this.host = duoDetails.host;
this.enabled = duoDetails.enabled;
}

/**
Expand All @@ -183,6 +238,6 @@ export class TwoFactorSetupDuoComponent
}

type TwoFactorDuoComponentConfig = {
authResponse: AuthResponse<TwoFactorDuoResponse>;
authResponse: TwoFactorSetupDialogData<TwoFactorDuoResponseUnion>;
organizationId?: string;
};
Loading
Loading