diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index bc55314a3a68..d7ad16e5303c 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -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"; @@ -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) { diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 2f6f0642b193..b708b7ed9649 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -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"; @@ -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 = await lastValueFrom( - twoFactorVerifyDialogRef.closed, - ); + const result: TwoFactorSetupDialogData = + await lastValueFrom(twoFactorVerifyDialogRef.closed); if (!result) { return; } diff --git a/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential-create-options.response.ts b/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential-create-options.response.ts index ce588207727e..89d2e1fa53b7 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential-create-options.response.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/response/webauthn-login-credential-create-options.response.ts @@ -1,4 +1,4 @@ -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"; /** @@ -6,7 +6,7 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; */ 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}. @@ -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"); } } diff --git a/apps/web/src/app/auth/core/views/credential-create-options.view.ts b/apps/web/src/app/auth/core/views/credential-create-options.view.ts index 0aef622abffe..0e0860e34a6b 100644 --- a/apps/web/src/app/auth/core/views/credential-create-options.view.ts +++ b/apps/web/src/app/auth/core/views/credential-create-options.view.ts @@ -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, ) {} } diff --git a/apps/web/src/app/auth/settings/account/change-email.component.spec.ts b/apps/web/src/app/auth/settings/account/change-email.component.spec.ts index 20faa861fc09..834ce2e788b2 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.spec.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.spec.ts @@ -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"; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts index 543c4236d893..4521d81d8466 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts @@ -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, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts index 1857430eadd6..2cbb059c1ebd 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts @@ -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"; @@ -85,7 +84,14 @@ export class TwoFactorSetupAuthenticatorComponent @Output() onChangeStatus = new EventEmitter(); 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; @@ -96,7 +102,7 @@ export class TwoFactorSetupAuthenticatorComponent }); constructor( - @Inject(DIALOG_DATA) protected data: AuthResponse, + @Inject(DIALOG_DATA) protected data: TwoFactorSetupDialogData, private dialogRef: DialogRef, twoFactorService: TwoFactorService, i18nService: I18nService, @@ -136,9 +142,9 @@ export class TwoFactorSetupAuthenticatorComponent this.formGroup.controls.token.markAsTouched(); } - async auth(authResponse: AuthResponse) { + async auth(authResponse: TwoFactorSetupDialogData) { super.auth(authResponse); - return this.processResponse(authResponse.response); + return this.processGetResponse(authResponse.response); } submit = async () => { @@ -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); } @@ -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({ @@ -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); @@ -238,7 +255,7 @@ export class TwoFactorSetupAuthenticatorComponent static open( dialogService: DialogService, - config: DialogConfig>, + config: DialogConfig>, ) { return dialogService.open(TwoFactorSetupAuthenticatorComponent, config); } diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts index bac4f7754860..9d10cadbf407 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts @@ -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"; @@ -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({ @@ -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, @@ -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; @@ -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( @@ -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; } /** @@ -183,6 +238,6 @@ export class TwoFactorSetupDuoComponent } type TwoFactorDuoComponentConfig = { - authResponse: AuthResponse; + authResponse: TwoFactorSetupDialogData; organizationId?: string; }; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts index fafc766d95c0..cd1ac2847061 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts @@ -6,11 +6,12 @@ import { firstValueFrom, map } from "rxjs"; 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 { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; -import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request"; -import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.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 { TwoFactorEmailDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-email-delete.request"; +import { TwoFactorEmailSetupRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-email-setup.request"; +import { TwoFactorEmailUpdateRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-email-update.request"; +import { TwoFactorEmailDetailsResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-email-details.response"; +import { TwoFactorEmailResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-email.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"; @@ -63,13 +64,21 @@ export class TwoFactorSetupEmailComponent sentEmail: string = ""; emailPromise: Promise | undefined; override componentName = "app-two-factor-email"; + private userVerificationToken: string | undefined; + + private requireUserVerificationToken(): string { + if (this.userVerificationToken === undefined) { + throw new Error("User verification token is missing"); + } + return this.userVerificationToken; + } formGroup = this.formBuilder.group({ token: ["", [Validators.required]], email: ["", [Validators.email, Validators.required]], }); constructor( - @Inject(DIALOG_DATA) protected data: AuthResponse, + @Inject(DIALOG_DATA) protected data: TwoFactorSetupDialogData, twoFactorService: TwoFactorService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -108,9 +117,9 @@ export class TwoFactorSetupEmailComponent await this.auth(this.data); } - auth(authResponse: AuthResponse) { + auth(authResponse: TwoFactorSetupDialogData) { super.auth(authResponse); - return this.processResponse(authResponse.response); + return this.processGetResponse(authResponse.response); } submit = async () => { @@ -129,35 +138,63 @@ export class TwoFactorSetupEmailComponent }; private disableEmail() { - return super.disableMethod(); + return this.disableMethod(); } sendEmail = async () => { - const request = await this.buildRequestModel(TwoFactorEmailRequest); - request.email = this.email; + const request = new TwoFactorEmailSetupRequest(this.email, this.requireUserVerificationToken()); this.emailPromise = this.twoFactorService.postTwoFactorEmailSetup(request); await this.emailPromise; this.sentEmail = this.email; }; protected async enable() { - const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest); - request.email = this.email; - request.token = this.token; + const request = new TwoFactorEmailUpdateRequest( + this.token, + this.email, + this.requireUserVerificationToken(), + ); const response = await this.twoFactorService.putTwoFactorEmail(request); - await this.processResponse(response); + await this.applyEmailDetails(response.email); 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; + } + + const request = new TwoFactorEmailDeleteRequest(this.requireUserVerificationToken()); + await this.twoFactorService.deleteTwoFactorEmail(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 async processResponse(response: TwoFactorEmailResponse) { + private async processGetResponse(response: TwoFactorEmailResponse) { + this.userVerificationToken = response.userVerificationToken; + await this.applyEmailDetails(response.email); + } + + private async applyEmailDetails(emailDetails: TwoFactorEmailDetailsResponse) { this.token = null; - this.email = response.email; - this.enabled = response.enabled; + this.email = emailDetails.email; + this.enabled = emailDetails.enabled; if (!this.enabled && (this.email == null || this.email === "")) { this.email = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), @@ -171,11 +208,11 @@ export class TwoFactorSetupEmailComponent */ static open( dialogService: DialogService, - config: DialogConfig>, + config: DialogConfig>, ) { - return dialogService.open>( + return dialogService.open>( TwoFactorSetupEmailComponent, - config as DialogConfig, boolean>, + config as DialogConfig, boolean>, ); } } diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts index 5494353449dd..ced976218b91 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts @@ -4,16 +4,22 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; -import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request"; -import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; -import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response"; +import { + TwoFactorService, + TwoFactorUserVerificationResult, +} from "@bitwarden/common/auth/two-factor"; 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"; import { DialogService, ToastService } from "@bitwarden/components"; +// TODO: PM-39385 - Remove this base class and replace with a more flexible composition-based approach. /** * Base class for two-factor setup components (ex: email, yubikey, webauthn, duo). + * + * Subclasses must implement `disableMethod()` themselves — each provider routes to its own + * per-provider DELETE endpoint via the threaded user-verification token, so there is no + * meaningful generic implementation to share. */ @Directive({}) export abstract class TwoFactorSetupMethodBaseComponent { @@ -41,41 +47,14 @@ export abstract class TwoFactorSetupMethodBaseComponent { protected toastService: ToastService, ) {} - protected auth(authResponse: AuthResponseBase) { - this.secret = authResponse.secret; - this.verificationType = authResponse.verificationType; + protected auth(verificationResult: TwoFactorUserVerificationResult) { + this.secret = verificationResult.secret; + this.verificationType = verificationResult.verificationType; this.authed = true; } - protected async disableMethod() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "disable" }, - content: { key: "twoStepDisableDesc" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - const request = await this.buildRequestModel(TwoFactorProviderRequest); - if (this.type === undefined) { - throw new Error("Two-factor provider type is required"); - } - request.type = this.type; - if (this.organizationId != null) { - await this.twoFactorService.putTwoFactorOrganizationDisable(this.organizationId, request); - } else { - await this.twoFactorService.putTwoFactorDisable(request); - } - this.enabled = false; - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("twoStepDisabled"), - }); - this.onUpdated.emit(false); - } + // TODO: PM-39385 - For each subclass, rename disable as delete since they are hard deletes. + protected abstract disableMethod(): Promise; protected async buildRequestModel( requestClass: new () => T, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts index 9fc6e94ec26b..bea0b7965f0e 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts @@ -5,15 +5,16 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angula import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; -import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request"; -import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn.request"; -import { - ChallengeResponse, - TwoFactorWebAuthnResponse, -} from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; -import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; -import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; +import { WebAuthnChallengeResponse } from "@bitwarden/common/auth/models/response/web-authn-challenge.response"; +import { TwoFactorService, TwoFactorSetupDialogData } from "@bitwarden/common/auth/two-factor"; +import { TwoFactorWebAuthnChallengeRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-web-authn-challenge.request"; +import { TwoFactorWebAuthnDeleteAllRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-web-authn-delete-all.request"; +import { TwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-web-authn-delete.request"; +import { TwoFactorWebAuthnUpdateRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-web-authn-update.request"; +import { TwoFactorWebAuthnChallengeResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-web-authn-challenge.response"; +import { TwoFactorWebAuthnDeleteResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-web-authn-delete.response"; +import { TwoFactorWebAuthnDetailsResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-web-authn-details.response"; +import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-web-authn.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"; @@ -41,7 +42,7 @@ interface Key { name: string; configured: boolean; migrated?: boolean; - removePromise: Promise | null; + removePromise: Promise | null; } // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush @@ -73,14 +74,22 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom webAuthnError: boolean = false; webAuthnListening: boolean = false; webAuthnResponse: PublicKeyCredential | null = null; - challengePromise: Promise | undefined; + challengePromise: Promise | undefined; + 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-webauthn"; protected formGroup: FormGroup; constructor( - @Inject(DIALOG_DATA) protected data: AuthResponse, + @Inject(DIALOG_DATA) protected data: TwoFactorSetupDialogData, private dialogRef: DialogRef, twoFactorService: TwoFactorService, i18nService: I18nService, @@ -106,9 +115,9 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom this.auth(data); } - auth(authResponse: AuthResponse) { + auth(authResponse: TwoFactorSetupDialogData) { super.auth(authResponse); - this.processResponse(authResponse.response); + this.processGetResponse(authResponse.response); } submit = async () => { @@ -120,24 +129,25 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom }; protected async enable() { - const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest); - if (this.webAuthnResponse == undefined || this.keyIdAvailable == undefined) { throw new Error("WebAuthn response or key ID is missing"); } - request.deviceResponse = this.webAuthnResponse; - request.id = this.keyIdAvailable; - request.name = this.formGroup.value.name || ""; + const request = new TwoFactorWebAuthnUpdateRequest( + this.webAuthnResponse, + this.formGroup.value.name || "", + this.keyIdAvailable, + this.requireUserVerificationToken(), + ); const response = await this.twoFactorService.putTwoFactorWebAuthn(request); - this.processResponse(response); + this.applyWebAuthnDetails(response.webAuthn); this.toastService.showToast({ title: this.i18nService.t("success"), message: this.i18nService.t("twoFactorProviderEnabled"), variant: "success", }); - this.onUpdated.emit(response.enabled); + this.onUpdated.emit(response.webAuthn.enabled); } disable = async () => { @@ -148,6 +158,31 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom } }; + protected override async disableMethod() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "disable" }, + content: { key: "twoStepDisableDesc" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + // Server's per-credential DELETE refuses to remove the last registered credential + // (lockout-prevention), so the only path to delete the WebAuthn enrollment entirely is the + // bulk endpoint. + const request = new TwoFactorWebAuthnDeleteAllRequest(this.requireUserVerificationToken()); + await this.twoFactorService.deleteTwoFactorWebAuthnAll(request); + this.enabled = false; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("twoStepDisabled"), + }); + this.onUpdated.emit(false); + } + async remove(key: Key) { if (this.keysConfiguredCount <= 1 || key.removePromise != null) { return; @@ -163,13 +198,12 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom if (!confirmed) { return; } - const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest); - request.id = key.id; + const request = new TwoFactorWebAuthnDeleteRequest(key.id, this.requireUserVerificationToken()); try { key.removePromise = this.twoFactorService.deleteTwoFactorWebAuthn(request); const response = await key.removePromise; key.removePromise = null; - await this.processResponse(response); + this.applyWebAuthnDetails(response.webAuthn); } catch (e) { this.logService.error(e); } @@ -179,13 +213,17 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom if (this.keyIdAvailable == null) { return; } - const request = await this.buildRequestModel(SecretVerificationRequest); + const request = new TwoFactorWebAuthnChallengeRequest(this.requireUserVerificationToken()); this.challengePromise = this.twoFactorService.getTwoFactorWebAuthnChallenge(request); - const challenge = await this.challengePromise; - this.readDevice(challenge); + const wrappedChallenge = await this.challengePromise; + if (wrappedChallenge.options == null) { + this.webAuthnError = true; + return; + } + this.readDevice(wrappedChallenge.options); }; - private readDevice(webAuthnChallenge: ChallengeResponse) { + private readDevice(webAuthnChallenge: WebAuthnChallengeResponse) { // eslint-disable-next-line console.log("listening for key..."); this.resetWebAuthn(true); @@ -227,9 +265,14 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom throw new Error("Unable to find next available key ID"); } - private processResponse(response: TwoFactorWebAuthnResponse) { - if (!response.keys || response.keys.length === 0) { - response.keys = []; + private processGetResponse(response: TwoFactorWebAuthnResponse) { + this.userVerificationToken = response.userVerificationToken; + this.applyWebAuthnDetails(response.webAuthn); + } + + private applyWebAuthnDetails(webAuthnDetails: TwoFactorWebAuthnDetailsResponse) { + if (!webAuthnDetails.keys || webAuthnDetails.keys.length === 0) { + webAuthnDetails.keys = []; } this.resetWebAuthn(); this.keys = []; @@ -242,7 +285,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom this.keysConfiguredCount = 0; // Build configured keys - for (const key of response.keys) { + for (const key of webAuthnDetails.keys) { this.keysConfiguredCount++; this.keys.push({ id: key.id, @@ -259,7 +302,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom // While we don't have any technical constraints _at this time_, we should avoid // unbounded growth of key IDs over time as users add/remove keys; // this strategy gap-fills key IDs. - const existingIds = new Set(response.keys.map((k) => k.id)); + const existingIds = new Set(webAuthnDetails.keys.map((k) => k.id)); const nextId = this.findNextAvailableKeyId(existingIds); // Add unconfigured slot, which can be used to add a new key @@ -271,17 +314,17 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom }); this.keyIdAvailable = nextId; - this.enabled = response.enabled; + this.enabled = webAuthnDetails.enabled; this.onUpdated.emit(this.enabled); } static open( dialogService: DialogService, - config: DialogConfig>, + config: DialogConfig>, ) { - return dialogService.open>( + return dialogService.open>( TwoFactorSetupWebAuthnComponent, - config as DialogConfig, boolean>, + config as DialogConfig, boolean>, ); } } diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts index b4d6aef5f46b..2e0578483779 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts @@ -11,10 +11,11 @@ import { import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.request"; -import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.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 { TwoFactorYubiKeyDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-yubikey-delete.request"; +import { TwoFactorYubiKeyUpdateRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-yubikey-update.request"; +import { TwoFactorYubiKeyDetailsResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-yubi-key-details.response"; +import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-yubi-key.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"; @@ -72,6 +73,14 @@ export class TwoFactorSetupYubiKeyComponent type = TwoFactorProviderType.Yubikey; keys: Key[] = []; anyKeyHasNfc = false; + 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-yubikey"; formGroup: @@ -90,7 +99,7 @@ export class TwoFactorSetupYubiKeyComponent } constructor( - @Inject(DIALOG_DATA) protected data: AuthResponse, + @Inject(DIALOG_DATA) protected data: TwoFactorSetupDialogData, twoFactorService: TwoFactorService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -135,9 +144,9 @@ export class TwoFactorSetupYubiKeyComponent }); } - auth(authResponse: AuthResponse) { + auth(authResponse: TwoFactorSetupDialogData) { super.auth(authResponse); - this.processResponse(authResponse.response); + this.processGetResponse(authResponse.response); } submit = async () => { @@ -166,15 +175,18 @@ export class TwoFactorSetupYubiKeyComponent return; } const keys = this.formGroup.controls.formKeys.value; - const request = await this.buildRequestModel(UpdateTwoFactorYubikeyOtpRequest); - request.key1 = keys != null && keys.length > 0 ? (keys[0]?.key ?? "") : ""; - request.key2 = keys != null && keys.length > 1 ? (keys[1]?.key ?? "") : ""; - request.key3 = keys != null && keys.length > 2 ? (keys[2]?.key ?? "") : ""; - request.key4 = keys != null && keys.length > 3 ? (keys[3]?.key ?? "") : ""; - request.key5 = keys != null && keys.length > 4 ? (keys[4]?.key ?? "") : ""; - request.nfc = this.formGroup.value.anyKeyHasNfc ?? false; - - this.processResponse(await this.twoFactorService.putTwoFactorYubiKey(request)); + const request = new TwoFactorYubiKeyUpdateRequest( + keys != null && keys.length > 0 ? (keys[0]?.key ?? "") : "", + keys != null && keys.length > 1 ? (keys[1]?.key ?? "") : "", + keys != null && keys.length > 2 ? (keys[2]?.key ?? "") : "", + keys != null && keys.length > 3 ? (keys[3]?.key ?? "") : "", + keys != null && keys.length > 4 ? (keys[4]?.key ?? "") : "", + this.formGroup.value.anyKeyHasNfc ?? false, + this.requireUserVerificationToken(), + ); + + const response = await this.twoFactorService.putTwoFactorYubiKey(request); + this.applyYubiKeyDetails(response.yubiKey); this.refreshFormArrayData(); this.toastService.showToast({ title: this.i18nService.t("success"), @@ -184,6 +196,28 @@ export class TwoFactorSetupYubiKeyComponent this.onUpdated.emit(this.enabled); } + protected override async disableMethod() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "disable" }, + content: { key: "twoStepDisableDesc" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + const request = new TwoFactorYubiKeyDeleteRequest(this.requireUserVerificationToken()); + await this.twoFactorService.deleteTwoFactorYubiKey(request); + this.enabled = false; + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("twoStepDisabled"), + }); + this.onUpdated.emit(false); + } + remove(pos: number) { this.keys[pos].key = ""; this.keys[pos].existingKey = ""; @@ -198,15 +232,20 @@ export class TwoFactorSetupYubiKeyComponent }); } - private processResponse(response: TwoFactorYubiKeyResponse) { - this.enabled = response.enabled; - this.anyKeyHasNfc = response.nfc || !response.enabled; + private processGetResponse(response: TwoFactorYubiKeyResponse) { + this.userVerificationToken = response.userVerificationToken; + this.applyYubiKeyDetails(response.yubiKey); + } + + private applyYubiKeyDetails(yubiKeyDetails: TwoFactorYubiKeyDetailsResponse) { + this.enabled = yubiKeyDetails.enabled; + this.anyKeyHasNfc = yubiKeyDetails.nfc || !yubiKeyDetails.enabled; this.keys = [ - { key: response.key1, existingKey: this.padRight(response.key1) }, - { key: response.key2, existingKey: this.padRight(response.key2) }, - { key: response.key3, existingKey: this.padRight(response.key3) }, - { key: response.key4, existingKey: this.padRight(response.key4) }, - { key: response.key5, existingKey: this.padRight(response.key5) }, + { key: yubiKeyDetails.key1, existingKey: this.padRight(yubiKeyDetails.key1) }, + { key: yubiKeyDetails.key2, existingKey: this.padRight(yubiKeyDetails.key2) }, + { key: yubiKeyDetails.key3, existingKey: this.padRight(yubiKeyDetails.key3) }, + { key: yubiKeyDetails.key4, existingKey: this.padRight(yubiKeyDetails.key4) }, + { key: yubiKeyDetails.key5, existingKey: this.padRight(yubiKeyDetails.key5) }, ]; } @@ -223,11 +262,11 @@ export class TwoFactorSetupYubiKeyComponent static open( dialogService: DialogService, - config: DialogConfig>, + config: DialogConfig>, ) { - return dialogService.open>( + return dialogService.open>( TwoFactorSetupYubiKeyComponent, - config as DialogConfig, boolean>, + config as DialogConfig, boolean>, ); } } diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 104feb474580..67a25c785454 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -20,15 +20,20 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga 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 { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request"; -import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response"; -import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response"; +import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { TwoFactorService, TwoFactorProviders } from "@bitwarden/common/auth/two-factor"; -import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; +import { + TwoFactorService, + TwoFactorProviders, + TwoFactorSetupDialogData, +} from "@bitwarden/common/auth/two-factor"; +import { TwoFactorDuoDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-duo-delete.request"; +import { TwoFactorYubiKeyDeleteRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-yubikey-delete.request"; +import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-authenticator.response"; +import { TwoFactorDuoResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-duo.response"; +import { TwoFactorEmailResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-email.response"; +import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/two-factor/response/two-factor-yubi-key.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -155,28 +160,47 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } /** - * For users who enabled a premium-only 2fa provider, - * they should still be allowed to disable that provider - * (without otherwise modifying) if they no longer have - * premium access [PM-21204] - * @param type the 2FA Provider Type + * Lapsed-premium escape hatch: a user who previously enrolled a premium provider (YubiKey or + * Duo) while subscribed should still be able to disable it after their premium subscription + * lapses. Surfaces a UV dialog rather than the full management screen so the user cannot + * accidentally attempt to add more credentials (which would fail at PUT-time on the server). + * + * Under the hood: the per-provider GET (now non-premium-gated) mints a UV token, which the + * per-provider DELETE then consumes — same single dialog interaction as before, two server + * round-trips instead of one. */ async disablePremium2faTypeForNonPremiumUser(type: TwoFactorProviderType) { - // Use UserVerificationDialogComponent instead of TwoFactorVerifyComponent - // because the latter makes GET API calls that require premium for YubiKey/Duo. - // The disable endpoint only requires user verification, not provider configuration. const result = await UserVerificationDialogComponent.open(this.dialogService, { title: "twoStepLogin", verificationType: { type: "custom", verificationFn: async (secret) => { - const request = await this.userVerificationService.buildRequest( - secret, - TwoFactorProviderRequest, - ); - request.type = type; + const getRequest = + await this.userVerificationService.buildRequest( + secret, + SecretVerificationRequest, + ); - await this.twoFactorService.putTwoFactorDisable(request); + switch (type) { + case TwoFactorProviderType.Yubikey: { + const response = await this.twoFactorService.getTwoFactorYubiKey(getRequest); + const deleteRequest = new TwoFactorYubiKeyDeleteRequest( + response.userVerificationToken, + ); + await this.twoFactorService.deleteTwoFactorYubiKey(deleteRequest); + break; + } + case TwoFactorProviderType.Duo: { + const response = await this.twoFactorService.getTwoFactorDuo(getRequest); + const deleteRequest = new TwoFactorDuoDeleteRequest(response.userVerificationToken); + await this.twoFactorService.deleteTwoFactorDuo(deleteRequest); + break; + } + default: + throw new Error( + "disablePremium2faTypeForNonPremiumUser only supports YubiKey and Duo", + ); + } return true; }, }, @@ -204,7 +228,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { switch (type) { case TwoFactorProviderType.Authenticator: { - const result: AuthResponse = + const result: TwoFactorSetupDialogData = await this.callTwoFactorVerifyDialog(type); if (!result) { return; @@ -222,7 +246,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { break; } case TwoFactorProviderType.Yubikey: { - const result: AuthResponse = + const result: TwoFactorSetupDialogData = await this.callTwoFactorVerifyDialog(type); if (!result) { return; @@ -239,7 +263,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { break; } case TwoFactorProviderType.Duo: { - const result: AuthResponse = + const result: TwoFactorSetupDialogData = await this.callTwoFactorVerifyDialog(type); if (!result) { return; @@ -261,7 +285,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { break; } case TwoFactorProviderType.Email: { - const result: AuthResponse = + const result: TwoFactorSetupDialogData = await this.callTwoFactorVerifyDialog(type); if (!result) { return; @@ -281,7 +305,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { break; } case TwoFactorProviderType.WebAuthn: { - const result: AuthResponse = + const result: TwoFactorSetupDialogData = await this.callTwoFactorVerifyDialog(type); if (!result) { return; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts index e922ddf610c5..b9faf84dadd3 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts @@ -5,9 +5,11 @@ import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular"; 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 { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; -import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; -import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; -import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response"; +import { + TwoFactorService, + TwoFactorSetupDialogData, + TwoFactorResponse, +} from "@bitwarden/common/auth/two-factor"; import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -132,9 +134,9 @@ export class TwoFactorVerifyComponent { } static open(dialogService: DialogService, config: DialogConfig) { - return dialogService.open, TwoFactorVerifyDialogData>( + return dialogService.open, TwoFactorVerifyDialogData>( TwoFactorVerifyComponent, - config as DialogConfig>, + config as DialogConfig>, ); } } diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index b3db3dbca4a3..20fee2d2a9c2 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -5,8 +5,8 @@ import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; -import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; +import { TwoFactorEmailLoginRequest } from "@bitwarden/common/auth/two-factor/request/two-factor-email-login.request"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -124,7 +124,7 @@ export class TwoFactorAuthEmailComponent implements OnInit { } // TODO: PM-17545 - consider building a method on the login strategy service to get a mostly - // initialized TwoFactorEmailRequest in 1 call instead of 5 like we do today. + // initialized TwoFactorEmailLoginRequest in 1 call instead of 5 like we do today. const email = await this.loginStrategyService.getEmail(); if (email == null) { @@ -137,7 +137,7 @@ export class TwoFactorAuthEmailComponent implements OnInit { } try { - const request = new TwoFactorEmailRequest(); + const request = new TwoFactorEmailLoginRequest(); request.email = email; request.masterPasswordHash = (await this.loginStrategyService.getMasterPasswordHash()) ?? ""; diff --git a/libs/common/src/auth/models/request/disable-two-factor-authenticator.request.ts b/libs/common/src/auth/models/request/disable-two-factor-authenticator.request.ts deleted file mode 100644 index 37337720e1f6..000000000000 --- a/libs/common/src/auth/models/request/disable-two-factor-authenticator.request.ts +++ /dev/null @@ -1,8 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { TwoFactorProviderRequest } from "./two-factor-provider.request"; - -export class DisableTwoFactorAuthenticatorRequest extends TwoFactorProviderRequest { - key: string; - userVerificationToken: string; -} diff --git a/libs/common/src/auth/models/request/two-factor-provider.request.ts b/libs/common/src/auth/models/request/two-factor-provider.request.ts deleted file mode 100644 index 05cec8c421f4..000000000000 --- a/libs/common/src/auth/models/request/two-factor-provider.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; - -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class TwoFactorProviderRequest extends SecretVerificationRequest { - type: TwoFactorProviderType; -} diff --git a/libs/common/src/auth/models/request/update-two-factor-authenticator.request.ts b/libs/common/src/auth/models/request/update-two-factor-authenticator.request.ts deleted file mode 100644 index 501802cb474b..000000000000 --- a/libs/common/src/auth/models/request/update-two-factor-authenticator.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class UpdateTwoFactorAuthenticatorRequest extends SecretVerificationRequest { - token: string; - key: string; - userVerificationToken: string; -} diff --git a/libs/common/src/auth/models/request/update-two-factor-duo.request.ts b/libs/common/src/auth/models/request/update-two-factor-duo.request.ts deleted file mode 100644 index 9d9d2596450f..000000000000 --- a/libs/common/src/auth/models/request/update-two-factor-duo.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class UpdateTwoFactorDuoRequest extends SecretVerificationRequest { - clientId: string; - clientSecret: string; - host: string; -} diff --git a/libs/common/src/auth/models/request/update-two-factor-email.request.ts b/libs/common/src/auth/models/request/update-two-factor-email.request.ts deleted file mode 100644 index 6baa0ed32fc5..000000000000 --- a/libs/common/src/auth/models/request/update-two-factor-email.request.ts +++ /dev/null @@ -1,8 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class UpdateTwoFactorEmailRequest extends SecretVerificationRequest { - token: string; - email: string; -} diff --git a/libs/common/src/auth/models/request/update-two-factor-web-authn-delete.request.ts b/libs/common/src/auth/models/request/update-two-factor-web-authn-delete.request.ts deleted file mode 100644 index 73068c1e7ddb..000000000000 --- a/libs/common/src/auth/models/request/update-two-factor-web-authn-delete.request.ts +++ /dev/null @@ -1,7 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class UpdateTwoFactorWebAuthnDeleteRequest extends SecretVerificationRequest { - id: number; -} diff --git a/libs/common/src/auth/models/request/update-two-factor-web-authn.request.ts b/libs/common/src/auth/models/request/update-two-factor-web-authn.request.ts deleted file mode 100644 index 57b1655eac14..000000000000 --- a/libs/common/src/auth/models/request/update-two-factor-web-authn.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class UpdateTwoFactorWebAuthnRequest extends SecretVerificationRequest { - deviceResponse: PublicKeyCredential; - name: string; - id: number; -} diff --git a/libs/common/src/auth/models/request/update-two-factor-yubikey-otp.request.ts b/libs/common/src/auth/models/request/update-two-factor-yubikey-otp.request.ts deleted file mode 100644 index 148e6c02fab6..000000000000 --- a/libs/common/src/auth/models/request/update-two-factor-yubikey-otp.request.ts +++ /dev/null @@ -1,12 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; - -export class UpdateTwoFactorYubikeyOtpRequest extends SecretVerificationRequest { - key1: string; - key2: string; - key3: string; - key4: string; - key5: string; - nfc: boolean; -} diff --git a/libs/common/src/auth/models/response/two-factor-web-authn.response.ts b/libs/common/src/auth/models/response/web-authn-challenge.response.ts similarity index 63% rename from libs/common/src/auth/models/response/two-factor-web-authn.response.ts rename to libs/common/src/auth/models/response/web-authn-challenge.response.ts index 855dfc06d5e9..02c6527876bc 100644 --- a/libs/common/src/auth/models/response/two-factor-web-authn.response.ts +++ b/libs/common/src/auth/models/response/web-authn-challenge.response.ts @@ -1,32 +1,17 @@ import { BaseResponse } from "../../../models/response/base.response"; import { Utils } from "../../../platform/misc/utils"; -export class TwoFactorWebAuthnResponse extends BaseResponse { - enabled: boolean; - keys: KeyResponse[]; - - constructor(response: any) { - super(response); - this.enabled = this.getResponseProperty("Enabled"); - const keys = this.getResponseProperty("Keys"); - this.keys = keys == null ? null : keys.map((k: any) => new KeyResponse(k)); - } -} - -export class KeyResponse extends BaseResponse { - name: string; - id: number; - migrated: boolean; - - constructor(response: any) { - super(response); - this.name = this.getResponseProperty("Name"); - this.id = this.getResponseProperty("Id"); - this.migrated = this.getResponseProperty("Migrated"); - } -} - -export class ChallengeResponse extends BaseResponse implements PublicKeyCredentialCreationOptions { +/** + * WebAuthn credential-creation options minted by the server. Implements the + * `PublicKeyCredentialCreationOptions` shape consumed by `navigator.credentials.create()`. + * + * Reused by both the 2FA settings flow (wrapped in `TwoFactorWebAuthnChallengeResponse`) + * and the passkey-login flow (wrapped in `WebauthnLoginCredentialCreateOptionsResponse`). + */ +export class WebAuthnChallengeResponse + extends BaseResponse + implements PublicKeyCredentialCreationOptions +{ attestation?: AttestationConveyancePreference; authenticatorSelection?: AuthenticatorSelectionCriteria; challenge: BufferSource; diff --git a/libs/common/src/auth/two-factor/abstractions/two-factor-api.service.ts b/libs/common/src/auth/two-factor/abstractions/two-factor-api.service.ts index e38f3d50d2ea..3c9e74bf444e 100644 --- a/libs/common/src/auth/two-factor/abstractions/two-factor-api.service.ts +++ b/libs/common/src/auth/two-factor/abstractions/two-factor-api.service.ts @@ -1,24 +1,36 @@ import { ListResponse } from "../../../models/response/list.response"; -import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; -import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request"; -import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request"; -import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request"; -import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request"; -import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request"; -import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request"; -import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request"; -import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request"; -import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response"; -import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response"; -import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response"; -import { - ChallengeResponse, - TwoFactorWebAuthnResponse, -} from "../../models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response"; +import { TwoFactorAuthenticatorDeleteRequest } from "../request/two-factor-authenticator-delete.request"; +import { TwoFactorAuthenticatorUpdateRequest } from "../request/two-factor-authenticator-update.request"; +import { TwoFactorDuoDeleteRequest } from "../request/two-factor-duo-delete.request"; +import { TwoFactorDuoUpdateRequest } from "../request/two-factor-duo-update.request"; +import { TwoFactorEmailDeleteRequest } from "../request/two-factor-email-delete.request"; +import { TwoFactorEmailLoginRequest } from "../request/two-factor-email-login.request"; +import { TwoFactorEmailSetupRequest } from "../request/two-factor-email-setup.request"; +import { TwoFactorEmailUpdateRequest } from "../request/two-factor-email-update.request"; +import { TwoFactorOrganizationDuoDeleteRequest } from "../request/two-factor-organization-duo-delete.request"; +import { TwoFactorWebAuthnChallengeRequest } from "../request/two-factor-web-authn-challenge.request"; +import { TwoFactorWebAuthnDeleteAllRequest } from "../request/two-factor-web-authn-delete-all.request"; +import { TwoFactorWebAuthnDeleteRequest } from "../request/two-factor-web-authn-delete.request"; +import { TwoFactorWebAuthnUpdateRequest } from "../request/two-factor-web-authn-update.request"; +import { TwoFactorYubiKeyDeleteRequest } from "../request/two-factor-yubikey-delete.request"; +import { TwoFactorYubiKeyUpdateRequest } from "../request/two-factor-yubikey-update.request"; +import { TwoFactorAuthenticatorUpdateResponse } from "../response/two-factor-authenticator-update.response"; +import { TwoFactorAuthenticatorResponse } from "../response/two-factor-authenticator.response"; +import { TwoFactorDuoUpdateResponse } from "../response/two-factor-duo-update.response"; +import { TwoFactorDuoResponse } from "../response/two-factor-duo.response"; +import { TwoFactorEmailUpdateResponse } from "../response/two-factor-email-update.response"; +import { TwoFactorEmailResponse } from "../response/two-factor-email.response"; +import { TwoFactorOrganizationDuoUpdateResponse } from "../response/two-factor-organization-duo-update.response"; +import { TwoFactorOrganizationDuoResponse } from "../response/two-factor-organization-duo.response"; +import { TwoFactorProviderResponse } from "../response/two-factor-provider.response"; +import { TwoFactorRecoverResponse } from "../response/two-factor-recover.response"; +import { TwoFactorWebAuthnChallengeResponse } from "../response/two-factor-web-authn-challenge.response"; +import { TwoFactorWebAuthnDeleteResponse } from "../response/two-factor-web-authn-delete.response"; +import { TwoFactorWebAuthnUpdateResponse } from "../response/two-factor-web-authn-update.response"; +import { TwoFactorWebAuthnResponse } from "../response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyUpdateResponse } from "../response/two-factor-yubi-key-update.response"; +import { TwoFactorYubiKeyResponse } from "../response/two-factor-yubi-key.response"; /** * Service abstraction for two-factor authentication API operations. @@ -26,7 +38,7 @@ import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi- * authenticator apps (TOTP), email, Duo, YubiKey, WebAuthn (FIDO2), and recovery codes. * * All methods that retrieve sensitive configuration data require user verification via - * SecretVerificationRequest. Premium-tier providers (Duo, YubiKey) require an active + * SecretVerificationRequest. Update/enable methods for Duo and YubiKey require an active * premium subscription. Organization-level methods require appropriate administrative permissions. */ export abstract class TwoFactorApiService { @@ -62,7 +74,7 @@ export abstract class TwoFactorApiService { /** * Gets the email two-factor configuration for the current user. - * Returns the configured email address and enabled status. + * Returns the configured email address, enabled status, and a user verification token. * Requires user verification via master password or OTP. * * @param request The secret verification request to authorize the operation. @@ -72,8 +84,8 @@ export abstract class TwoFactorApiService { /** * Gets the Duo two-factor configuration for the current user. - * Returns Duo integration configuration details. - * Requires user verification and an active premium subscription. + * Returns Duo integration configuration details and a user verification token. + * Requires user verification via master password or OTP. * * @param request The secret verification request to authorize the operation. * @returns A promise that resolves to the Duo configuration. @@ -82,7 +94,7 @@ export abstract class TwoFactorApiService { /** * Gets the Duo two-factor configuration for an organization. - * Returns organization-level Duo integration configuration. + * Returns organization-level Duo integration configuration and a user verification token. * Requires user verification and organization policy management permissions. * * @param organizationId The ID of the organization. @@ -92,12 +104,12 @@ export abstract class TwoFactorApiService { abstract getTwoFactorOrganizationDuo( organizationId: string, request: SecretVerificationRequest, - ): Promise; + ): Promise; /** * Gets the YubiKey OTP two-factor configuration for the current user. - * Returns configured YubiKey device identifiers (multiple keys supported). - * Requires user verification and an active premium subscription. + * Returns configured YubiKey device identifiers and a user verification token. + * Requires user verification via master password or OTP. * * @param request The secret verification request to authorize the operation. * @returns A promise that resolves to the YubiKey configuration. @@ -108,7 +120,8 @@ export abstract class TwoFactorApiService { /** * Gets the WebAuthn (FIDO2) two-factor configuration for the current user. - * Returns a list of registered WebAuthn credentials with their names and IDs. + * Returns a list of registered WebAuthn credentials with their names and IDs, + * and a user verification token. * Requires user verification via master password or OTP. * * @param request The secret verification request to authorize the operation. @@ -121,15 +134,16 @@ export abstract class TwoFactorApiService { /** * Gets a WebAuthn challenge for registering a new WebAuthn credential. * This must be called before putTwoFactorWebAuthn to obtain the cryptographic challenge - * required for credential creation. The challenge is used by the browser's WebAuthn API. - * Requires user verification via master password or OTP. + * required for credential creation. Authorized by replaying the user-verification token + * minted by the prior getTwoFactorWebAuthn call; that same token stays valid for the + * subsequent PUT. * - * @param request The secret verification request to authorize the operation. - * @returns A promise that resolves to the credential creation options containing the challenge. + * @param request The request carrying the user-verification token from getTwoFactorWebAuthn. + * @returns A promise that resolves to the wrapped challenge response. */ abstract getTwoFactorWebAuthnChallenge( - request: SecretVerificationRequest, - ): Promise; + request: TwoFactorWebAuthnChallengeRequest, + ): Promise; /** * Gets the recovery code configuration for the current user. @@ -153,44 +167,61 @@ export abstract class TwoFactorApiService { * @returns A promise that resolves to the updated authenticator configuration. */ abstract putTwoFactorAuthenticator( - request: UpdateTwoFactorAuthenticatorRequest, - ): Promise; + request: TwoFactorAuthenticatorUpdateRequest, + ): Promise; /** - * Disables the authenticator (TOTP) two-factor provider for the current user. - * Requires user verification token to confirm the operation. + * Removes the authenticator (TOTP) two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. * - * @param request The request containing verification credentials to disable the provider. - * @returns A promise that resolves to the updated provider status. + * @param request The request containing the user verification token and key. */ abstract deleteTwoFactorAuthenticator( - request: DisableTwoFactorAuthenticatorRequest, - ): Promise; + request: TwoFactorAuthenticatorDeleteRequest, + ): Promise; /** * Enables or updates the email two-factor provider. * Validates the email verification token sent via postTwoFactorEmailSetup before enabling. - * The token must match the code sent to the specified email address. * * @param request The request containing the email configuration and verification token. * @returns A promise that resolves to the updated email two-factor configuration. */ - abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise; + abstract putTwoFactorEmail( + request: TwoFactorEmailUpdateRequest, + ): Promise; + + /** + * Removes the email two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. + * + * @param request The request containing the user verification token. + */ + abstract deleteTwoFactorEmail(request: TwoFactorEmailDeleteRequest): Promise; /** * Enables or updates the Duo two-factor provider for the current user. * Validates the Duo configuration (client ID, client secret, and host) before enabling. - * Requires user verification and an active premium subscription. + * Requires an active premium subscription. * * @param request The request containing the Duo integration configuration. * @returns A promise that resolves to the updated Duo configuration. */ - abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise; + abstract putTwoFactorDuo(request: TwoFactorDuoUpdateRequest): Promise; + + /** + * Removes the Duo two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. + * Does NOT require premium — deletion must always be available even if premium has lapsed. + * + * @param request The request containing the user verification token. + */ + abstract deleteTwoFactorDuo(request: TwoFactorDuoDeleteRequest): Promise; /** * Enables or updates the Duo two-factor provider for an organization. * Validates the Duo configuration (client ID, client secret, and host) before enabling. - * Requires user verification and organization policy management permissions. + * Requires organization policy management permissions. * * @param organizationId The ID of the organization. * @param request The request containing the Duo integration configuration. @@ -198,86 +229,91 @@ export abstract class TwoFactorApiService { */ abstract putTwoFactorOrganizationDuo( organizationId: string, - request: UpdateTwoFactorDuoRequest, - ): Promise; + request: TwoFactorDuoUpdateRequest, + ): Promise; + + /** + * Removes the Duo two-factor enrollment for an organization. Returns 204 No Content. + * Requires a user verification token to confirm the operation and + * organization policy management permissions. + * + * @param organizationId The ID of the organization. + * @param request The request containing the user verification token. + */ + abstract deleteTwoFactorOrganizationDuo( + organizationId: string, + request: TwoFactorOrganizationDuoDeleteRequest, + ): Promise; /** * Enables or updates the YubiKey OTP two-factor provider. * Validates each provided YubiKey by testing an OTP from the device. * Supports up to 5 YubiKey devices. Empty key slots are allowed. - * Requires user verification and an active premium subscription. - * Includes a 2-second delay on validation failure to prevent timing attacks. + * Requires an active premium subscription. * * @param request The request containing YubiKey device identifiers and test OTPs. * @returns A promise that resolves to the updated YubiKey configuration. */ abstract putTwoFactorYubiKey( - request: UpdateTwoFactorYubikeyOtpRequest, - ): Promise; + request: TwoFactorYubiKeyUpdateRequest, + ): Promise; + + /** + * Removes the YubiKey two-factor enrollment for the current user. Returns 204 No Content. + * Requires a user verification token to confirm the operation. + * Does NOT require premium — deletion must always be available even if premium has lapsed. + * + * @param request The request containing the user verification token. + */ + abstract deleteTwoFactorYubiKey(request: TwoFactorYubiKeyDeleteRequest): Promise; /** * Registers a new WebAuthn (FIDO2) credential for two-factor authentication. * Must be called after getTwoFactorWebAuthnChallenge to complete the registration flow. * The device response contains the signed challenge from the authenticator device. - * Requires user verification via master password or OTP. * - * @param request The request containing the WebAuthn credential creation response from the browser. + * @param request The request containing the WebAuthn credential creation response and verification token. * @returns A promise that resolves to the updated WebAuthn configuration with the new credential. */ abstract putTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnRequest, - ): Promise; + request: TwoFactorWebAuthnUpdateRequest, + ): Promise; /** * Removes a specific WebAuthn (FIDO2) credential from the user's account. * The credential will no longer be usable for two-factor authentication. * Other registered WebAuthn credentials remain active. - * Requires user verification via master password or OTP. + * Server refuses to remove the last registered credential — use deleteTwoFactorWebAuthnAll instead. + * The operation modifies the WebAuthn provider rather than destroying it, so the response + * carries the updated parent state. * - * @param request The request containing the credential ID to remove. + * @param request The request containing the credential ID and verification token. * @returns A promise that resolves to the updated WebAuthn configuration. */ abstract deleteTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnDeleteRequest, - ): Promise; + request: TwoFactorWebAuthnDeleteRequest, + ): Promise; /** - * Disables a specific two-factor provider for the current user. - * The provider will no longer be required or usable for authentication. - * Requires user verification via master password or OTP. + * Removes the entire WebAuthn (FIDO2) two-factor enrollment for the current user — all + * credentials are deleted in a single round-trip. The only path that can clear the last + * registered credential, since per-credential delete refuses by design. Returns 204 No Content. * - * @param request The request specifying which provider type to disable. - * @returns A promise that resolves to the updated provider status. + * @param request The request containing the user verification token. */ - abstract putTwoFactorDisable( - request: TwoFactorProviderRequest, - ): Promise; - - /** - * Disables a specific two-factor provider for an organization. - * The provider will no longer be available for organization members. - * Requires user verification and organization policy management permissions. - * - * @param organizationId The ID of the organization. - * @param request The request specifying which provider type to disable. - * @returns A promise that resolves to the updated provider status. - */ - abstract putTwoFactorOrganizationDisable( - organizationId: string, - request: TwoFactorProviderRequest, - ): Promise; + abstract deleteTwoFactorWebAuthnAll(request: TwoFactorWebAuthnDeleteAllRequest): Promise; /** * Initiates email two-factor setup by sending a verification code to the specified email address. * This is the first step in enabling email two-factor authentication. * The verification code must be provided to putTwoFactorEmail to complete setup. * Only used during initial configuration, not during login flows. - * Requires user verification via master password or OTP. + * Requires a user verification token (from a prior getTwoFactorEmail call). * - * @param request The request containing the email address for two-factor setup. + * @param request The request containing the email address and verification token for two-factor setup. * @returns A promise that resolves when the verification email has been sent. */ - abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise; + abstract postTwoFactorEmailSetup(request: TwoFactorEmailSetupRequest): Promise; /** * Sends a two-factor authentication code via email during the login flow. @@ -288,5 +324,5 @@ export abstract class TwoFactorApiService { * @param request The request to send the two-factor code, optionally including SSO or auth request tokens. * @returns A promise that resolves when the authentication email has been sent. */ - abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; + abstract postTwoFactorEmail(request: TwoFactorEmailLoginRequest): Promise; } diff --git a/libs/common/src/auth/two-factor/abstractions/two-factor.service.ts b/libs/common/src/auth/two-factor/abstractions/two-factor.service.ts index 4cba40716e10..10acf0507e17 100644 --- a/libs/common/src/auth/two-factor/abstractions/two-factor.service.ts +++ b/libs/common/src/auth/two-factor/abstractions/two-factor.service.ts @@ -1,27 +1,39 @@ import { ListResponse } from "../../../models/response/list.response"; import { KeyDefinition, TWO_FACTOR_MEMORY } from "../../../platform/state"; import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; -import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; -import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request"; -import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request"; -import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request"; -import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request"; -import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request"; -import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request"; -import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request"; -import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request"; import { IdentityTwoFactorResponse } from "../../models/response/identity-two-factor.response"; -import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response"; -import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response"; -import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response"; -import { - ChallengeResponse, - TwoFactorWebAuthnResponse, -} from "../../models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response"; +import { TwoFactorAuthenticatorDeleteRequest } from "../request/two-factor-authenticator-delete.request"; +import { TwoFactorAuthenticatorUpdateRequest } from "../request/two-factor-authenticator-update.request"; +import { TwoFactorDuoDeleteRequest } from "../request/two-factor-duo-delete.request"; +import { TwoFactorDuoUpdateRequest } from "../request/two-factor-duo-update.request"; +import { TwoFactorEmailDeleteRequest } from "../request/two-factor-email-delete.request"; +import { TwoFactorEmailLoginRequest } from "../request/two-factor-email-login.request"; +import { TwoFactorEmailSetupRequest } from "../request/two-factor-email-setup.request"; +import { TwoFactorEmailUpdateRequest } from "../request/two-factor-email-update.request"; +import { TwoFactorOrganizationDuoDeleteRequest } from "../request/two-factor-organization-duo-delete.request"; +import { TwoFactorWebAuthnChallengeRequest } from "../request/two-factor-web-authn-challenge.request"; +import { TwoFactorWebAuthnDeleteAllRequest } from "../request/two-factor-web-authn-delete-all.request"; +import { TwoFactorWebAuthnDeleteRequest } from "../request/two-factor-web-authn-delete.request"; +import { TwoFactorWebAuthnUpdateRequest } from "../request/two-factor-web-authn-update.request"; +import { TwoFactorYubiKeyDeleteRequest } from "../request/two-factor-yubikey-delete.request"; +import { TwoFactorYubiKeyUpdateRequest } from "../request/two-factor-yubikey-update.request"; +import { TwoFactorAuthenticatorUpdateResponse } from "../response/two-factor-authenticator-update.response"; +import { TwoFactorAuthenticatorResponse } from "../response/two-factor-authenticator.response"; +import { TwoFactorDuoUpdateResponse } from "../response/two-factor-duo-update.response"; +import { TwoFactorDuoResponse } from "../response/two-factor-duo.response"; +import { TwoFactorEmailUpdateResponse } from "../response/two-factor-email-update.response"; +import { TwoFactorEmailResponse } from "../response/two-factor-email.response"; +import { TwoFactorOrganizationDuoUpdateResponse } from "../response/two-factor-organization-duo-update.response"; +import { TwoFactorOrganizationDuoResponse } from "../response/two-factor-organization-duo.response"; +import { TwoFactorProviderResponse } from "../response/two-factor-provider.response"; +import { TwoFactorRecoverResponse } from "../response/two-factor-recover.response"; +import { TwoFactorWebAuthnChallengeResponse } from "../response/two-factor-web-authn-challenge.response"; +import { TwoFactorWebAuthnDeleteResponse } from "../response/two-factor-web-authn-delete.response"; +import { TwoFactorWebAuthnUpdateResponse } from "../response/two-factor-web-authn-update.response"; +import { TwoFactorWebAuthnResponse } from "../response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyUpdateResponse } from "../response/two-factor-yubi-key-update.response"; +import { TwoFactorYubiKeyResponse } from "../response/two-factor-yubi-key.response"; /** * Metadata and display information for a two-factor authentication provider. @@ -269,7 +281,7 @@ export abstract class TwoFactorService { abstract getTwoFactorOrganizationDuo( organizationId: string, request: SecretVerificationRequest, - ): Promise; + ): Promise; /** * Gets the YubiKey OTP two-factor configuration for the current user from the API. @@ -300,17 +312,17 @@ export abstract class TwoFactorService { /** * Gets a WebAuthn challenge for registering a new WebAuthn credential from the API. * This must be called before putTwoFactorWebAuthn to obtain the cryptographic challenge - * required for credential creation. The challenge is used by the browser's WebAuthn API. - * Requires user verification via master password or OTP. + * required for credential creation. Authorized by replaying the user-verification token + * minted by the prior getTwoFactorWebAuthn call (token-replay chain step); that same + * token stays valid for the subsequent PUT. * Used for settings management. * - * @param request The {@link SecretVerificationRequest} to prove authentication. + * @param request The {@link TwoFactorWebAuthnChallengeRequest} carrying the user-verification token. * @returns A promise that resolves to the credential creation options containing the challenge. - * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ abstract getTwoFactorWebAuthnChallenge( - request: SecretVerificationRequest, - ): Promise; + request: TwoFactorWebAuthnChallengeRequest, + ): Promise; /** * Gets the recovery code configuration for the current user from the API. @@ -332,26 +344,24 @@ export abstract class TwoFactorService { * The token must be generated by an authenticator app using the secret key. * Used for settings management. * - * @param request The {@link UpdateTwoFactorAuthenticatorRequest} to prove authentication. + * @param request The {@link TwoFactorAuthenticatorUpdateRequest} to prove authentication. * @returns A promise that resolves to the updated authenticator configuration. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ abstract putTwoFactorAuthenticator( - request: UpdateTwoFactorAuthenticatorRequest, - ): Promise; + request: TwoFactorAuthenticatorUpdateRequest, + ): Promise; /** - * Disables the authenticator (TOTP) two-factor provider for the current user. - * Requires user verification token to confirm the operation. + * Removes the authenticator (TOTP) two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. * Used for settings management. * - * @param request The {@link DisableTwoFactorAuthenticatorRequest} to prove authentication. - * @returns A promise that resolves to the updated provider status. - * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. + * @param request The {@link TwoFactorAuthenticatorDeleteRequest} containing the user verification token and key. */ abstract deleteTwoFactorAuthenticator( - request: DisableTwoFactorAuthenticatorRequest, - ): Promise; + request: TwoFactorAuthenticatorDeleteRequest, + ): Promise; /** * Enables or updates the email two-factor provider for the current user. @@ -359,11 +369,13 @@ export abstract class TwoFactorService { * The token must match the code sent to the specified email address. * Used for settings management. * - * @param request The {@link UpdateTwoFactorEmailRequest} to prove authentication. + * @param request The {@link TwoFactorEmailUpdateRequest} to prove authentication. * @returns A promise that resolves to the updated email two-factor configuration. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ - abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise; + abstract putTwoFactorEmail( + request: TwoFactorEmailUpdateRequest, + ): Promise; /** * Enables or updates the Duo two-factor provider for the current user. @@ -371,11 +383,11 @@ export abstract class TwoFactorService { * Requires user verification and an active premium subscription. * Used for settings management. * - * @param request The {@link UpdateTwoFactorDuoRequest} to prove authentication. + * @param request The {@link TwoFactorDuoUpdateRequest} to prove authentication. * @returns A promise that resolves to the updated Duo configuration. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ - abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise; + abstract putTwoFactorDuo(request: TwoFactorDuoUpdateRequest): Promise; /** * Enables or updates the Duo two-factor provider for an organization. @@ -384,14 +396,14 @@ export abstract class TwoFactorService { * Used for settings management. * * @param organizationId The ID of the organization. - * @param request The {@link UpdateTwoFactorDuoRequest} to prove authentication. + * @param request The {@link TwoFactorDuoUpdateRequest} to prove authentication. * @returns A promise that resolves to the updated organization Duo configuration. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ abstract putTwoFactorOrganizationDuo( organizationId: string, - request: UpdateTwoFactorDuoRequest, - ): Promise; + request: TwoFactorDuoUpdateRequest, + ): Promise; /** * Enables or updates the YubiKey OTP two-factor provider for the current user. @@ -400,13 +412,13 @@ export abstract class TwoFactorService { * Requires user verification and an active premium subscription. * Used for settings management. * - * @param request The {@link UpdateTwoFactorYubikeyOtpRequest} to prove authentication. + * @param request The {@link TwoFactorYubiKeyUpdateRequest} to prove authentication. * @returns A promise that resolves to the updated YubiKey configuration. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ abstract putTwoFactorYubiKey( - request: UpdateTwoFactorYubikeyOtpRequest, - ): Promise; + request: TwoFactorYubiKeyUpdateRequest, + ): Promise; /** * Registers a new WebAuthn (FIDO2) credential for two-factor authentication for the current user. @@ -415,13 +427,13 @@ export abstract class TwoFactorService { * Requires user verification via master password or OTP. * Used for settings management. * - * @param request The {@link UpdateTwoFactorWebAuthnRequest} to prove authentication. + * @param request The {@link TwoFactorWebAuthnUpdateRequest} to prove authentication. * @returns A promise that resolves to the updated WebAuthn configuration with the new credential. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ abstract putTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnRequest, - ): Promise; + request: TwoFactorWebAuthnUpdateRequest, + ): Promise; /** * Removes a specific WebAuthn (FIDO2) credential from the user's account. @@ -430,43 +442,67 @@ export abstract class TwoFactorService { * Requires user verification via master password or OTP. * Used for settings management. * - * @param request The {@link UpdateTwoFactorWebAuthnDeleteRequest} to prove authentication. + * @param request The {@link TwoFactorWebAuthnDeleteRequest} to prove authentication. * @returns A promise that resolves to the updated WebAuthn configuration. * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ abstract deleteTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnDeleteRequest, - ): Promise; + request: TwoFactorWebAuthnDeleteRequest, + ): Promise; /** - * Disables a specific two-factor provider for the current user. - * The provider will no longer be required or usable for authentication. - * Requires user verification via master password or OTP. + * Removes the YubiKey two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. + * Does NOT require premium — deletion must always be available even if premium has lapsed. * Used for settings management. * - * @param request The {@link TwoFactorProviderRequest} to prove authentication. - * @returns A promise that resolves to the updated provider status. - * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. + * @param request The {@link TwoFactorYubiKeyDeleteRequest} containing the user verification token. */ - abstract putTwoFactorDisable( - request: TwoFactorProviderRequest, - ): Promise; + abstract deleteTwoFactorYubiKey(request: TwoFactorYubiKeyDeleteRequest): Promise; /** - * Disables a specific two-factor provider for an organization. - * The provider will no longer be available for organization members. - * Requires user verification and organization policy management permissions. + * Removes the Duo two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. + * Does NOT require premium — deletion must always be available even if premium has lapsed. + * Used for settings management. + * + * @param request The {@link TwoFactorDuoDeleteRequest} containing the user verification token. + */ + abstract deleteTwoFactorDuo(request: TwoFactorDuoDeleteRequest): Promise; + + /** + * Removes the email two-factor enrollment for the current user. + * Requires a user verification token to confirm the operation. Returns 204 No Content. + * Used for settings management. + * + * @param request The {@link TwoFactorEmailDeleteRequest} containing the user verification token. + */ + abstract deleteTwoFactorEmail(request: TwoFactorEmailDeleteRequest): Promise; + + /** + * Removes the Duo two-factor enrollment for an organization. Returns 204 No Content. + * Requires a user verification token to confirm the operation and + * organization policy management permissions. * Used for settings management. * * @param organizationId The ID of the organization. - * @param request The {@link TwoFactorProviderRequest} to prove authentication. - * @returns A promise that resolves to the updated provider status. - * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. + * @param request The {@link TwoFactorOrganizationDuoDeleteRequest} containing the user verification token. */ - abstract putTwoFactorOrganizationDisable( + abstract deleteTwoFactorOrganizationDuo( organizationId: string, - request: TwoFactorProviderRequest, - ): Promise; + request: TwoFactorOrganizationDuoDeleteRequest, + ): Promise; + + /** + * Removes the entire WebAuthn (FIDO2) two-factor enrollment for the current user — all + * credentials are deleted in a single round-trip. Returns 204 No Content. The only path + * that can clear the last registered credential, since per-credential delete refuses by + * design. + * Used for settings management. + * + * @param request The {@link TwoFactorWebAuthnDeleteAllRequest} containing the user verification token. + */ + abstract deleteTwoFactorWebAuthnAll(request: TwoFactorWebAuthnDeleteAllRequest): Promise; /** * Initiates email two-factor setup by sending a verification code to the specified email address. @@ -476,11 +512,10 @@ export abstract class TwoFactorService { * Requires user verification via master password or OTP. * Used for settings management. * - * @param request The {@link TwoFactorEmailRequest} to prove authentication. + * @param request The {@link TwoFactorEmailSetupRequest} to prove authentication. * @returns A promise that resolves when the verification email has been sent. - * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ - abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise; + abstract postTwoFactorEmailSetup(request: TwoFactorEmailSetupRequest): Promise; /** * Sends a two-factor authentication code via email during the login flow. @@ -489,9 +524,8 @@ export abstract class TwoFactorService { * May be called without authentication for login scenarios. * Used during authentication flows. * - * @param request The {@link TwoFactorEmailRequest} to prove authentication. + * @param request The {@link TwoFactorEmailLoginRequest} to prove authentication. * @returns A promise that resolves when the authentication email has been sent. - * @remarks Use {@link UserVerificationService.buildRequest} to create the request object. */ - abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; + abstract postTwoFactorEmail(request: TwoFactorEmailLoginRequest): Promise; } diff --git a/libs/common/src/auth/two-factor/index.ts b/libs/common/src/auth/two-factor/index.ts index fd6edf0ac3c3..29764d54f456 100644 --- a/libs/common/src/auth/two-factor/index.ts +++ b/libs/common/src/auth/two-factor/index.ts @@ -1,2 +1,5 @@ export * from "./abstractions"; +export * from "./request"; +export * from "./response"; export * from "./services"; +export * from "./types"; diff --git a/libs/common/src/auth/two-factor/request/index.ts b/libs/common/src/auth/two-factor/request/index.ts new file mode 100644 index 000000000000..e7e627a57fe9 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/index.ts @@ -0,0 +1,15 @@ +export * from "./two-factor-authenticator-delete.request"; +export * from "./two-factor-authenticator-update.request"; +export * from "./two-factor-duo-delete.request"; +export * from "./two-factor-duo-update.request"; +export * from "./two-factor-email-delete.request"; +export * from "./two-factor-email-login.request"; +export * from "./two-factor-email-setup.request"; +export * from "./two-factor-email-update.request"; +export * from "./two-factor-organization-duo-delete.request"; +export * from "./two-factor-web-authn-challenge.request"; +export * from "./two-factor-web-authn-delete-all.request"; +export * from "./two-factor-web-authn-delete.request"; +export * from "./two-factor-web-authn-update.request"; +export * from "./two-factor-yubikey-delete.request"; +export * from "./two-factor-yubikey-update.request"; diff --git a/libs/common/src/auth/two-factor/request/two-factor-authenticator-delete.request.ts b/libs/common/src/auth/two-factor/request/two-factor-authenticator-delete.request.ts new file mode 100644 index 000000000000..8262921fdd5c --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-authenticator-delete.request.ts @@ -0,0 +1,6 @@ +export class TwoFactorAuthenticatorDeleteRequest { + constructor( + public key: string, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-authenticator-update.request.ts b/libs/common/src/auth/two-factor/request/two-factor-authenticator-update.request.ts new file mode 100644 index 000000000000..f4fc67557c33 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-authenticator-update.request.ts @@ -0,0 +1,7 @@ +export class TwoFactorAuthenticatorUpdateRequest { + constructor( + public token: string, + public key: string, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-duo-delete.request.ts b/libs/common/src/auth/two-factor/request/two-factor-duo-delete.request.ts new file mode 100644 index 000000000000..d315e912543e --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-duo-delete.request.ts @@ -0,0 +1,3 @@ +export class TwoFactorDuoDeleteRequest { + constructor(public userVerificationToken: string) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-duo-update.request.ts b/libs/common/src/auth/two-factor/request/two-factor-duo-update.request.ts new file mode 100644 index 000000000000..ae0fb101917f --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-duo-update.request.ts @@ -0,0 +1,8 @@ +export class TwoFactorDuoUpdateRequest { + constructor( + public clientId: string, + public clientSecret: string, + public host: string, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-email-delete.request.ts b/libs/common/src/auth/two-factor/request/two-factor-email-delete.request.ts new file mode 100644 index 000000000000..7ef7315bd2c5 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-email-delete.request.ts @@ -0,0 +1,3 @@ +export class TwoFactorEmailDeleteRequest { + constructor(public userVerificationToken: string) {} +} diff --git a/libs/common/src/auth/models/request/two-factor-email.request.ts b/libs/common/src/auth/two-factor/request/two-factor-email-login.request.ts similarity index 54% rename from libs/common/src/auth/models/request/two-factor-email.request.ts rename to libs/common/src/auth/two-factor/request/two-factor-email-login.request.ts index bdff27cd7004..d75d6b5e2bcc 100644 --- a/libs/common/src/auth/models/request/two-factor-email.request.ts +++ b/libs/common/src/auth/two-factor/request/two-factor-email-login.request.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { SecretVerificationRequest } from "./secret-verification.request"; +import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; -export class TwoFactorEmailRequest extends SecretVerificationRequest { +export class TwoFactorEmailLoginRequest extends SecretVerificationRequest { email: string; deviceIdentifier: string; authRequestId: string; diff --git a/libs/common/src/auth/two-factor/request/two-factor-email-setup.request.ts b/libs/common/src/auth/two-factor/request/two-factor-email-setup.request.ts new file mode 100644 index 000000000000..aefcf2eec1a5 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-email-setup.request.ts @@ -0,0 +1,6 @@ +export class TwoFactorEmailSetupRequest { + constructor( + public email: string, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-email-update.request.ts b/libs/common/src/auth/two-factor/request/two-factor-email-update.request.ts new file mode 100644 index 000000000000..8646edfe97cf --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-email-update.request.ts @@ -0,0 +1,7 @@ +export class TwoFactorEmailUpdateRequest { + constructor( + public token: string, + public email: string, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-organization-duo-delete.request.ts b/libs/common/src/auth/two-factor/request/two-factor-organization-duo-delete.request.ts new file mode 100644 index 000000000000..aff7ff49bd50 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-organization-duo-delete.request.ts @@ -0,0 +1,3 @@ +export class TwoFactorOrganizationDuoDeleteRequest { + constructor(public userVerificationToken: string) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-web-authn-challenge.request.ts b/libs/common/src/auth/two-factor/request/two-factor-web-authn-challenge.request.ts new file mode 100644 index 000000000000..255248c7a77f --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-web-authn-challenge.request.ts @@ -0,0 +1,3 @@ +export class TwoFactorWebAuthnChallengeRequest { + constructor(public userVerificationToken: string) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-web-authn-delete-all.request.ts b/libs/common/src/auth/two-factor/request/two-factor-web-authn-delete-all.request.ts new file mode 100644 index 000000000000..24edff3ed6bf --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-web-authn-delete-all.request.ts @@ -0,0 +1,3 @@ +export class TwoFactorWebAuthnDeleteAllRequest { + constructor(public userVerificationToken: string) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-web-authn-delete.request.ts b/libs/common/src/auth/two-factor/request/two-factor-web-authn-delete.request.ts new file mode 100644 index 000000000000..206ce7e312af --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-web-authn-delete.request.ts @@ -0,0 +1,6 @@ +export class TwoFactorWebAuthnDeleteRequest { + constructor( + public id: number, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-web-authn-update.request.ts b/libs/common/src/auth/two-factor/request/two-factor-web-authn-update.request.ts new file mode 100644 index 000000000000..5c4d4f7f2680 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-web-authn-update.request.ts @@ -0,0 +1,8 @@ +export class TwoFactorWebAuthnUpdateRequest { + constructor( + public deviceResponse: PublicKeyCredential, + public name: string, + public id: number, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-yubikey-delete.request.ts b/libs/common/src/auth/two-factor/request/two-factor-yubikey-delete.request.ts new file mode 100644 index 000000000000..f6d0daee4972 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-yubikey-delete.request.ts @@ -0,0 +1,3 @@ +export class TwoFactorYubiKeyDeleteRequest { + constructor(public userVerificationToken: string) {} +} diff --git a/libs/common/src/auth/two-factor/request/two-factor-yubikey-update.request.ts b/libs/common/src/auth/two-factor/request/two-factor-yubikey-update.request.ts new file mode 100644 index 000000000000..4ac1fa4ba984 --- /dev/null +++ b/libs/common/src/auth/two-factor/request/two-factor-yubikey-update.request.ts @@ -0,0 +1,11 @@ +export class TwoFactorYubiKeyUpdateRequest { + constructor( + public key1: string, + public key2: string, + public key3: string, + public key4: string, + public key5: string, + public nfc: boolean, + public userVerificationToken: string, + ) {} +} diff --git a/libs/common/src/auth/two-factor/response/index.ts b/libs/common/src/auth/two-factor/response/index.ts new file mode 100644 index 000000000000..5d52dcf9d927 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/index.ts @@ -0,0 +1,21 @@ +export * from "./two-factor-authenticator-details.response"; +export * from "./two-factor-authenticator-update.response"; +export * from "./two-factor-authenticator.response"; +export * from "./two-factor-duo-details.response"; +export * from "./two-factor-duo-update.response"; +export * from "./two-factor-duo.response"; +export * from "./two-factor-email-details.response"; +export * from "./two-factor-email-update.response"; +export * from "./two-factor-email.response"; +export * from "./two-factor-organization-duo-update.response"; +export * from "./two-factor-organization-duo.response"; +export * from "./two-factor-provider.response"; +export * from "./two-factor-recover.response"; +export * from "./two-factor-web-authn-challenge.response"; +export * from "./two-factor-web-authn-delete.response"; +export * from "./two-factor-web-authn-details.response"; +export * from "./two-factor-web-authn-update.response"; +export * from "./two-factor-web-authn.response"; +export * from "./two-factor-yubi-key-details.response"; +export * from "./two-factor-yubi-key-update.response"; +export * from "./two-factor-yubi-key.response"; diff --git a/libs/common/src/auth/models/response/two-factor-authenticator.response.ts b/libs/common/src/auth/two-factor/response/two-factor-authenticator-details.response.ts similarity index 56% rename from libs/common/src/auth/models/response/two-factor-authenticator.response.ts rename to libs/common/src/auth/two-factor/response/two-factor-authenticator-details.response.ts index f8ca1092be98..0cab8d6c9859 100644 --- a/libs/common/src/auth/models/response/two-factor-authenticator.response.ts +++ b/libs/common/src/auth/two-factor/response/two-factor-authenticator-details.response.ts @@ -1,14 +1,16 @@ import { BaseResponse } from "../../../models/response/base.response"; -export class TwoFactorAuthenticatorResponse extends BaseResponse { +/** + * Authenticator (TOTP) provider details. Embedded by the per-action + * `TwoFactorAuthenticator{Get,Update}Response` wrappers. + */ +export class TwoFactorAuthenticatorDetailsResponse extends BaseResponse { enabled: boolean; key: string; - userVerificationToken: string; constructor(response: any) { super(response); this.enabled = this.getResponseProperty("Enabled"); this.key = this.getResponseProperty("Key"); - this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); } } diff --git a/libs/common/src/auth/two-factor/response/two-factor-authenticator-update.response.ts b/libs/common/src/auth/two-factor/response/two-factor-authenticator-update.response.ts new file mode 100644 index 000000000000..f45dcf7af5c4 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-authenticator-update.response.ts @@ -0,0 +1,17 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorAuthenticatorDetailsResponse } from "./two-factor-authenticator-details.response"; + +/** + * Response from updating a user's authenticator (TOTP) two factor provider data. + */ +export class TwoFactorAuthenticatorUpdateResponse extends BaseResponse { + authenticator: TwoFactorAuthenticatorDetailsResponse; + + constructor(response: any) { + super(response); + this.authenticator = new TwoFactorAuthenticatorDetailsResponse( + this.getResponseProperty("Authenticator"), + ); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-authenticator.response.ts b/libs/common/src/auth/two-factor/response/two-factor-authenticator.response.ts new file mode 100644 index 000000000000..98dd77ba52ee --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-authenticator.response.ts @@ -0,0 +1,19 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorAuthenticatorDetailsResponse } from "./two-factor-authenticator-details.response"; + +/** + * Response from retrieving a user's authenticator (TOTP) two factor provider data. + */ +export class TwoFactorAuthenticatorResponse extends BaseResponse { + authenticator: TwoFactorAuthenticatorDetailsResponse; + userVerificationToken: string; + + constructor(response: any) { + super(response); + this.authenticator = new TwoFactorAuthenticatorDetailsResponse( + this.getResponseProperty("Authenticator"), + ); + this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); + } +} diff --git a/libs/common/src/auth/models/response/two-factor-duo.response.ts b/libs/common/src/auth/two-factor/response/two-factor-duo-details.response.ts similarity index 62% rename from libs/common/src/auth/models/response/two-factor-duo.response.ts rename to libs/common/src/auth/two-factor/response/two-factor-duo-details.response.ts index a195aa236dda..0320e61191a2 100644 --- a/libs/common/src/auth/models/response/two-factor-duo.response.ts +++ b/libs/common/src/auth/two-factor/response/two-factor-duo-details.response.ts @@ -1,16 +1,20 @@ import { BaseResponse } from "../../../models/response/base.response"; -export class TwoFactorDuoResponse extends BaseResponse { +/** + * Duo provider details for both user and organization scopes. Embedded by the per-action + * `TwoFactorDuo{Get,Update}Response` and `TwoFactorOrganizationDuo{Get,Update}Response` wrappers. + */ +export class TwoFactorDuoDetailsResponse extends BaseResponse { enabled: boolean; host: string; - clientSecret: string; clientId: string; + clientSecret: string; constructor(response: any) { super(response); this.enabled = this.getResponseProperty("Enabled"); this.host = this.getResponseProperty("Host"); - this.clientSecret = this.getResponseProperty("ClientSecret"); this.clientId = this.getResponseProperty("ClientId"); + this.clientSecret = this.getResponseProperty("ClientSecret"); } } diff --git a/libs/common/src/auth/two-factor/response/two-factor-duo-update.response.ts b/libs/common/src/auth/two-factor/response/two-factor-duo-update.response.ts new file mode 100644 index 000000000000..f1d5dfee7294 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-duo-update.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorDuoDetailsResponse } from "./two-factor-duo-details.response"; + +/** + * Response from updating a user's Duo two factor provider data. + */ +export class TwoFactorDuoUpdateResponse extends BaseResponse { + duo: TwoFactorDuoDetailsResponse; + + constructor(response: any) { + super(response); + this.duo = new TwoFactorDuoDetailsResponse(this.getResponseProperty("Duo")); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-duo.response.ts b/libs/common/src/auth/two-factor/response/two-factor-duo.response.ts new file mode 100644 index 000000000000..b1aa7f20b24e --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-duo.response.ts @@ -0,0 +1,17 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorDuoDetailsResponse } from "./two-factor-duo-details.response"; + +/** + * Response from retrieving a user's Duo two factor provider data. + */ +export class TwoFactorDuoResponse extends BaseResponse { + duo: TwoFactorDuoDetailsResponse; + userVerificationToken: string; + + constructor(response: any) { + super(response); + this.duo = new TwoFactorDuoDetailsResponse(this.getResponseProperty("Duo")); + this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); + } +} diff --git a/libs/common/src/auth/models/response/two-factor-email.response.ts b/libs/common/src/auth/two-factor/response/two-factor-email-details.response.ts similarity index 60% rename from libs/common/src/auth/models/response/two-factor-email.response.ts rename to libs/common/src/auth/two-factor/response/two-factor-email-details.response.ts index a0c81745feed..351fc616af7c 100644 --- a/libs/common/src/auth/models/response/two-factor-email.response.ts +++ b/libs/common/src/auth/two-factor/response/two-factor-email-details.response.ts @@ -1,6 +1,10 @@ import { BaseResponse } from "../../../models/response/base.response"; -export class TwoFactorEmailResponse extends BaseResponse { +/** + * Email provider details. Embedded by the per-action + * `TwoFactorEmail{Get,Update}Response` wrappers. + */ +export class TwoFactorEmailDetailsResponse extends BaseResponse { enabled: boolean; email: string; diff --git a/libs/common/src/auth/two-factor/response/two-factor-email-update.response.ts b/libs/common/src/auth/two-factor/response/two-factor-email-update.response.ts new file mode 100644 index 000000000000..019b6bfff4c7 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-email-update.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorEmailDetailsResponse } from "./two-factor-email-details.response"; + +/** + * Response from updating a user's email two factor provider data. + */ +export class TwoFactorEmailUpdateResponse extends BaseResponse { + email: TwoFactorEmailDetailsResponse; + + constructor(response: any) { + super(response); + this.email = new TwoFactorEmailDetailsResponse(this.getResponseProperty("Email")); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-email.response.ts b/libs/common/src/auth/two-factor/response/two-factor-email.response.ts new file mode 100644 index 000000000000..122de1c54739 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-email.response.ts @@ -0,0 +1,17 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorEmailDetailsResponse } from "./two-factor-email-details.response"; + +/** + * Response from retrieving a user's email two factor provider data. + */ +export class TwoFactorEmailResponse extends BaseResponse { + email: TwoFactorEmailDetailsResponse; + userVerificationToken: string; + + constructor(response: any) { + super(response); + this.email = new TwoFactorEmailDetailsResponse(this.getResponseProperty("Email")); + this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-organization-duo-update.response.ts b/libs/common/src/auth/two-factor/response/two-factor-organization-duo-update.response.ts new file mode 100644 index 000000000000..588f5e21b807 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-organization-duo-update.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorDuoDetailsResponse } from "./two-factor-duo-details.response"; + +/** + * Response from updating an organization's Duo two factor provider data. + */ +export class TwoFactorOrganizationDuoUpdateResponse extends BaseResponse { + duo: TwoFactorDuoDetailsResponse; + + constructor(response: any) { + super(response); + this.duo = new TwoFactorDuoDetailsResponse(this.getResponseProperty("Duo")); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-organization-duo.response.ts b/libs/common/src/auth/two-factor/response/two-factor-organization-duo.response.ts new file mode 100644 index 000000000000..9d705e0477ad --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-organization-duo.response.ts @@ -0,0 +1,17 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorDuoDetailsResponse } from "./two-factor-duo-details.response"; + +/** + * Response from retrieving an organization's Duo two factor provider data. + */ +export class TwoFactorOrganizationDuoResponse extends BaseResponse { + duo: TwoFactorDuoDetailsResponse; + userVerificationToken: string; + + constructor(response: any) { + super(response); + this.duo = new TwoFactorDuoDetailsResponse(this.getResponseProperty("Duo")); + this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); + } +} diff --git a/libs/common/src/auth/models/response/two-factor-provider.response.ts b/libs/common/src/auth/two-factor/response/two-factor-provider.response.ts similarity index 100% rename from libs/common/src/auth/models/response/two-factor-provider.response.ts rename to libs/common/src/auth/two-factor/response/two-factor-provider.response.ts diff --git a/libs/common/src/auth/models/response/two-factor-recover.response.ts b/libs/common/src/auth/two-factor/response/two-factor-recover.response.ts similarity index 100% rename from libs/common/src/auth/models/response/two-factor-recover.response.ts rename to libs/common/src/auth/two-factor/response/two-factor-recover.response.ts diff --git a/libs/common/src/auth/two-factor/response/two-factor-web-authn-challenge.response.ts b/libs/common/src/auth/two-factor/response/two-factor-web-authn-challenge.response.ts new file mode 100644 index 000000000000..d61c3e8b2130 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-web-authn-challenge.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "../../../models/response/base.response"; +import { WebAuthnChallengeResponse } from "../../models/response/web-authn-challenge.response"; + +/** + * Response from requesting a WebAuthn (FIDO2) credential creation challenge for two factor setup. + */ +export class TwoFactorWebAuthnChallengeResponse extends BaseResponse { + options: WebAuthnChallengeResponse | null; + + constructor(response: any) { + super(response); + const options = this.getResponseProperty("Options"); + this.options = options == null ? null : new WebAuthnChallengeResponse(options); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-web-authn-delete.response.ts b/libs/common/src/auth/two-factor/response/two-factor-web-authn-delete.response.ts new file mode 100644 index 000000000000..cddf3194ef62 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-web-authn-delete.response.ts @@ -0,0 +1,16 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorWebAuthnDetailsResponse } from "./two-factor-web-authn-details.response"; + +/** + * Response from removing a single credential from a user's WebAuthn (FIDO2) two factor + * provider, returning the updated provider data. + */ +export class TwoFactorWebAuthnDeleteResponse extends BaseResponse { + webAuthn: TwoFactorWebAuthnDetailsResponse; + + constructor(response: any) { + super(response); + this.webAuthn = new TwoFactorWebAuthnDetailsResponse(this.getResponseProperty("WebAuthn")); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-web-authn-details.response.ts b/libs/common/src/auth/two-factor/response/two-factor-web-authn-details.response.ts new file mode 100644 index 000000000000..3b1ea09d5a18 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-web-authn-details.response.ts @@ -0,0 +1,30 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +/** + * WebAuthn provider details. Embedded by the per-action + * `TwoFactorWebAuthn{Get,Update,Delete}Response` wrappers. + */ +export class TwoFactorWebAuthnDetailsResponse extends BaseResponse { + enabled: boolean; + keys: WebAuthnKeyResponse[]; + + constructor(response: any) { + super(response); + this.enabled = this.getResponseProperty("Enabled"); + const keys = this.getResponseProperty("Keys"); + this.keys = keys == null ? null : keys.map((k: any) => new WebAuthnKeyResponse(k)); + } +} + +export class WebAuthnKeyResponse extends BaseResponse { + name: string; + id: number; + migrated: boolean; + + constructor(response: any) { + super(response); + this.name = this.getResponseProperty("Name"); + this.id = this.getResponseProperty("Id"); + this.migrated = this.getResponseProperty("Migrated"); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-web-authn-update.response.ts b/libs/common/src/auth/two-factor/response/two-factor-web-authn-update.response.ts new file mode 100644 index 000000000000..335521be2a95 --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-web-authn-update.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorWebAuthnDetailsResponse } from "./two-factor-web-authn-details.response"; + +/** + * Response from updating a user's WebAuthn (FIDO2) two factor provider data. + */ +export class TwoFactorWebAuthnUpdateResponse extends BaseResponse { + webAuthn: TwoFactorWebAuthnDetailsResponse; + + constructor(response: any) { + super(response); + this.webAuthn = new TwoFactorWebAuthnDetailsResponse(this.getResponseProperty("WebAuthn")); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-web-authn.response.ts b/libs/common/src/auth/two-factor/response/two-factor-web-authn.response.ts new file mode 100644 index 000000000000..3be300366fce --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-web-authn.response.ts @@ -0,0 +1,17 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorWebAuthnDetailsResponse } from "./two-factor-web-authn-details.response"; + +/** + * Response from retrieving a user's WebAuthn (FIDO2) two factor provider data. + */ +export class TwoFactorWebAuthnResponse extends BaseResponse { + webAuthn: TwoFactorWebAuthnDetailsResponse; + userVerificationToken: string; + + constructor(response: any) { + super(response); + this.webAuthn = new TwoFactorWebAuthnDetailsResponse(this.getResponseProperty("WebAuthn")); + this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); + } +} diff --git a/libs/common/src/auth/models/response/two-factor-yubi-key.response.ts b/libs/common/src/auth/two-factor/response/two-factor-yubi-key-details.response.ts similarity index 76% rename from libs/common/src/auth/models/response/two-factor-yubi-key.response.ts rename to libs/common/src/auth/two-factor/response/two-factor-yubi-key-details.response.ts index cfdf41cce719..342bf20aee6a 100644 --- a/libs/common/src/auth/models/response/two-factor-yubi-key.response.ts +++ b/libs/common/src/auth/two-factor/response/two-factor-yubi-key-details.response.ts @@ -1,6 +1,10 @@ import { BaseResponse } from "../../../models/response/base.response"; -export class TwoFactorYubiKeyResponse extends BaseResponse { +/** + * YubiKey provider details. Embedded by the per-action + * `TwoFactorYubiKey{Get,Update}Response` wrappers. + */ +export class TwoFactorYubiKeyDetailsResponse extends BaseResponse { enabled: boolean; key1: string; key2: string; diff --git a/libs/common/src/auth/two-factor/response/two-factor-yubi-key-update.response.ts b/libs/common/src/auth/two-factor/response/two-factor-yubi-key-update.response.ts new file mode 100644 index 000000000000..9d8874637dca --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-yubi-key-update.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorYubiKeyDetailsResponse } from "./two-factor-yubi-key-details.response"; + +/** + * Response from updating a user's YubiKey two factor provider data. + */ +export class TwoFactorYubiKeyUpdateResponse extends BaseResponse { + yubiKey: TwoFactorYubiKeyDetailsResponse; + + constructor(response: any) { + super(response); + this.yubiKey = new TwoFactorYubiKeyDetailsResponse(this.getResponseProperty("YubiKey")); + } +} diff --git a/libs/common/src/auth/two-factor/response/two-factor-yubi-key.response.ts b/libs/common/src/auth/two-factor/response/two-factor-yubi-key.response.ts new file mode 100644 index 000000000000..86d696fb516a --- /dev/null +++ b/libs/common/src/auth/two-factor/response/two-factor-yubi-key.response.ts @@ -0,0 +1,17 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +import { TwoFactorYubiKeyDetailsResponse } from "./two-factor-yubi-key-details.response"; + +/** + * Response from retrieving a user's YubiKey two factor provider data. + */ +export class TwoFactorYubiKeyResponse extends BaseResponse { + yubiKey: TwoFactorYubiKeyDetailsResponse; + userVerificationToken: string; + + constructor(response: any) { + super(response); + this.yubiKey = new TwoFactorYubiKeyDetailsResponse(this.getResponseProperty("YubiKey")); + this.userVerificationToken = this.getResponseProperty("UserVerificationToken"); + } +} diff --git a/libs/common/src/auth/two-factor/services/default-two-factor-api.service.spec.ts b/libs/common/src/auth/two-factor/services/default-two-factor-api.service.spec.ts index 6cc30845b98c..c41a050aafb7 100644 --- a/libs/common/src/auth/two-factor/services/default-two-factor-api.service.spec.ts +++ b/libs/common/src/auth/two-factor/services/default-two-factor-api.service.spec.ts @@ -2,26 +2,38 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "../../../abstractions/api.service"; import { ListResponse } from "../../../models/response/list.response"; -import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; -import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request"; -import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request"; -import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request"; -import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request"; -import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request"; -import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request"; -import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request"; -import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request"; -import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response"; -import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response"; -import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response"; -import { - TwoFactorWebAuthnResponse, - ChallengeResponse, -} from "../../models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response"; +import { TwoFactorAuthenticatorDeleteRequest } from "../request/two-factor-authenticator-delete.request"; +import { TwoFactorAuthenticatorUpdateRequest } from "../request/two-factor-authenticator-update.request"; +import { TwoFactorDuoDeleteRequest } from "../request/two-factor-duo-delete.request"; +import { TwoFactorDuoUpdateRequest } from "../request/two-factor-duo-update.request"; +import { TwoFactorEmailDeleteRequest } from "../request/two-factor-email-delete.request"; +import { TwoFactorEmailLoginRequest } from "../request/two-factor-email-login.request"; +import { TwoFactorEmailSetupRequest } from "../request/two-factor-email-setup.request"; +import { TwoFactorEmailUpdateRequest } from "../request/two-factor-email-update.request"; +import { TwoFactorOrganizationDuoDeleteRequest } from "../request/two-factor-organization-duo-delete.request"; +import { TwoFactorWebAuthnChallengeRequest } from "../request/two-factor-web-authn-challenge.request"; +import { TwoFactorWebAuthnDeleteAllRequest } from "../request/two-factor-web-authn-delete-all.request"; +import { TwoFactorWebAuthnDeleteRequest } from "../request/two-factor-web-authn-delete.request"; +import { TwoFactorWebAuthnUpdateRequest } from "../request/two-factor-web-authn-update.request"; +import { TwoFactorYubiKeyDeleteRequest } from "../request/two-factor-yubikey-delete.request"; +import { TwoFactorYubiKeyUpdateRequest } from "../request/two-factor-yubikey-update.request"; +import { TwoFactorAuthenticatorUpdateResponse } from "../response/two-factor-authenticator-update.response"; +import { TwoFactorAuthenticatorResponse } from "../response/two-factor-authenticator.response"; +import { TwoFactorDuoUpdateResponse } from "../response/two-factor-duo-update.response"; +import { TwoFactorDuoResponse } from "../response/two-factor-duo.response"; +import { TwoFactorEmailUpdateResponse } from "../response/two-factor-email-update.response"; +import { TwoFactorEmailResponse } from "../response/two-factor-email.response"; +import { TwoFactorOrganizationDuoUpdateResponse } from "../response/two-factor-organization-duo-update.response"; +import { TwoFactorOrganizationDuoResponse } from "../response/two-factor-organization-duo.response"; +import { TwoFactorProviderResponse } from "../response/two-factor-provider.response"; +import { TwoFactorRecoverResponse } from "../response/two-factor-recover.response"; +import { TwoFactorWebAuthnChallengeResponse } from "../response/two-factor-web-authn-challenge.response"; +import { TwoFactorWebAuthnDeleteResponse } from "../response/two-factor-web-authn-delete.response"; +import { TwoFactorWebAuthnUpdateResponse } from "../response/two-factor-web-authn-update.response"; +import { TwoFactorWebAuthnResponse } from "../response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyUpdateResponse } from "../response/two-factor-yubi-key-update.response"; +import { TwoFactorYubiKeyResponse } from "../response/two-factor-yubi-key.response"; import { DefaultTwoFactorApiService } from "./default-two-factor-api.service"; @@ -89,8 +101,11 @@ describe("TwoFactorApiService", () => { const request = new SecretVerificationRequest(); request.masterPasswordHash = "master-password-hash"; const mockResponse = { - Enabled: false, - Key: "MFRGGZDFMZTWQ2LK", + Authenticator: { + Enabled: false, + Key: "MFRGGZDFMZTWQ2LK", + }, + UserVerificationToken: "uv-token", }; apiService.send.mockResolvedValue(mockResponse); @@ -104,18 +119,20 @@ describe("TwoFactorApiService", () => { true, ); expect(result).toBeInstanceOf(TwoFactorAuthenticatorResponse); - expect(result.enabled).toBe(false); + expect(result.authenticator.enabled).toBe(false); + expect(result.authenticator.key).toBe("MFRGGZDFMZTWQ2LK"); + expect(result.userVerificationToken).toBe("uv-token"); }); }); describe("putTwoFactorAuthenticator", () => { it("enables authenticator after validating the provided token", async () => { - const request = new UpdateTwoFactorAuthenticatorRequest(); - request.token = "123456"; - request.key = "MFRGGZDFMZTWQ2LK"; + const request = new TwoFactorAuthenticatorUpdateRequest("123456", "MFRGGZDFMZTWQ2LK", ""); const mockResponse = { - Enabled: true, - Key: "MFRGGZDFMZTWQ2LK", + Authenticator: { + Enabled: true, + Key: "MFRGGZDFMZTWQ2LK", + }, }; apiService.send.mockResolvedValue(mockResponse); @@ -128,34 +145,25 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorAuthenticatorResponse); - expect(result.enabled).toBe(true); - expect(result.key).toBeDefined(); + expect(result).toBeInstanceOf(TwoFactorAuthenticatorUpdateResponse); + expect(result.authenticator.enabled).toBe(true); + expect(result.authenticator.key).toBeDefined(); }); }); describe("deleteTwoFactorAuthenticator", () => { - it("disables authenticator two-factor authentication", async () => { - const request = new DisableTwoFactorAuthenticatorRequest(); - request.masterPasswordHash = "master-password-hash"; - const mockResponse = { - Enabled: false, - Type: 0, - }; - apiService.send.mockResolvedValue(mockResponse); + it("disables authenticator two-factor authentication and expects no body", async () => { + const request = new TwoFactorAuthenticatorDeleteRequest("MFRGGZDFMZTWQ2LK", "uv-token"); - const result = await twoFactorApiService.deleteTwoFactorAuthenticator(request); + await twoFactorApiService.deleteTwoFactorAuthenticator(request); expect(apiService.send).toHaveBeenCalledWith( "DELETE", "/two-factor/authenticator", request, true, - true, + false, ); - expect(result).toBeInstanceOf(TwoFactorProviderResponse); - expect(result.enabled).toBe(false); - expect(result.type).toBe(0); // Authenticator }); }); }); @@ -166,8 +174,11 @@ describe("TwoFactorApiService", () => { const request = new SecretVerificationRequest(); request.masterPasswordHash = "master-password-hash"; const mockResponse = { - Enabled: true, - Email: "user@example.com", + Email: { + Enabled: true, + Email: "user@example.com", + }, + UserVerificationToken: "uv-token", }; apiService.send.mockResolvedValue(mockResponse); @@ -181,16 +192,18 @@ describe("TwoFactorApiService", () => { true, ); expect(result).toBeInstanceOf(TwoFactorEmailResponse); - expect(result.enabled).toBe(true); - expect(result.email).toBeDefined(); + expect(result.email.enabled).toBe(true); + expect(result.email.email).toBe("user@example.com"); + expect(result.userVerificationToken).toBe("uv-token"); }); }); describe("postTwoFactorEmailSetup", () => { it("sends verification code to email address during two-factor setup", async () => { - const request = new TwoFactorEmailRequest(); - request.email = "user@example.com"; - request.masterPasswordHash = "master-password-hash"; + const request = new TwoFactorEmailSetupRequest( + "user@example.com", + "user-verification-token", + ); await twoFactorApiService.postTwoFactorEmailSetup(request); @@ -206,9 +219,8 @@ describe("TwoFactorApiService", () => { describe("postTwoFactorEmail", () => { it("sends two-factor authentication code during login flow", async () => { - const request = new TwoFactorEmailRequest(); + const request = new TwoFactorEmailLoginRequest(); request.email = "user@example.com"; - // Note: masterPasswordHash not required for login flow await twoFactorApiService.postTwoFactorEmail(request); @@ -224,12 +236,16 @@ describe("TwoFactorApiService", () => { describe("putTwoFactorEmail", () => { it("enables email two-factor after validating the verification code", async () => { - const request = new UpdateTwoFactorEmailRequest(); - request.email = "user@example.com"; - request.token = "verification-code"; + const request = new TwoFactorEmailUpdateRequest( + "verification-code", + "user@example.com", + "", + ); const mockResponse = { - Enabled: true, - Email: "user@example.com", + Email: { + Enabled: true, + Email: "user@example.com", + }, }; apiService.send.mockResolvedValue(mockResponse); @@ -242,9 +258,9 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorEmailResponse); - expect(result.enabled).toBe(true); - expect(result.email).toBeDefined(); + expect(result).toBeInstanceOf(TwoFactorEmailUpdateResponse); + expect(result.email.enabled).toBe(true); + expect(result.email.email).toBe("user@example.com"); }); }); }); @@ -255,10 +271,13 @@ describe("TwoFactorApiService", () => { const request = new SecretVerificationRequest(); request.masterPasswordHash = "master-password-hash"; const mockResponse = { - Enabled: true, - Host: "api-abc123.duosecurity.com", - ClientId: "DI9ABC1DEFGH2JKL", - ClientSecret: "client******", + Duo: { + Enabled: true, + Host: "api-abc123.duosecurity.com", + ClientId: "DI9ABC1DEFGH2JKL", + ClientSecret: "client******", + }, + UserVerificationToken: "uv-token", }; apiService.send.mockResolvedValue(mockResponse); @@ -272,10 +291,11 @@ describe("TwoFactorApiService", () => { true, ); expect(result).toBeInstanceOf(TwoFactorDuoResponse); - expect(result.enabled).toBe(true); - expect(result.host).toBeDefined(); - expect(result.clientId).toBeDefined(); - expect(result.clientSecret).toContain("******"); + expect(result.duo.enabled).toBe(true); + expect(result.duo.host).toBe("api-abc123.duosecurity.com"); + expect(result.duo.clientId).toBe("DI9ABC1DEFGH2JKL"); + expect(result.duo.clientSecret).toContain("******"); + expect(result.userVerificationToken).toBe("uv-token"); }); }); @@ -285,10 +305,13 @@ describe("TwoFactorApiService", () => { const request = new SecretVerificationRequest(); request.masterPasswordHash = "master-password-hash"; const mockResponse = { - Enabled: true, - Host: "api-xyz789.duosecurity.com", - ClientId: "DI4XYZ9MNOP3QRS", - ClientSecret: "orgcli******", + Duo: { + Enabled: true, + Host: "api-xyz789.duosecurity.com", + ClientId: "DI4XYZ9MNOP3QRS", + ClientSecret: "orgcli******", + }, + UserVerificationToken: "uv-token", }; apiService.send.mockResolvedValue(mockResponse); @@ -304,51 +327,60 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorDuoResponse); - expect(result.enabled).toBe(true); - expect(result.host).toBeDefined(); - expect(result.clientId).toBeDefined(); - expect(result.clientSecret).toContain("******"); + expect(result).toBeInstanceOf(TwoFactorOrganizationDuoResponse); + expect(result.duo.enabled).toBe(true); + expect(result.duo.host).toBe("api-xyz789.duosecurity.com"); + expect(result.duo.clientId).toBe("DI4XYZ9MNOP3QRS"); + expect(result.duo.clientSecret).toContain("******"); + expect(result.userVerificationToken).toBe("uv-token"); }); }); describe("putTwoFactorDuo", () => { it("enables Duo two-factor for premium user with valid integration details", async () => { - const request = new UpdateTwoFactorDuoRequest(); - request.host = "api-abc123.duosecurity.com"; - request.clientId = "DI9ABC1DEFGH2JKL"; - request.clientSecret = "client-secret-value-here"; + const request = new TwoFactorDuoUpdateRequest( + "DI9ABC1DEFGH2JKL", + "client-secret-value-here", + "api-abc123.duosecurity.com", + "", + ); const mockResponse = { - Enabled: true, - Host: "api-abc123.duosecurity.com", - ClientId: "DI9ABC1DEFGH2JKL", - ClientSecret: "client******", + Duo: { + Enabled: true, + Host: "api-abc123.duosecurity.com", + ClientId: "DI9ABC1DEFGH2JKL", + ClientSecret: "client******", + }, }; apiService.send.mockResolvedValue(mockResponse); const result = await twoFactorApiService.putTwoFactorDuo(request); expect(apiService.send).toHaveBeenCalledWith("PUT", "/two-factor/duo", request, true, true); - expect(result).toBeInstanceOf(TwoFactorDuoResponse); - expect(result.enabled).toBe(true); - expect(result.host).toBeDefined(); - expect(result.clientId).toBeDefined(); - expect(result.clientSecret).toContain("******"); + expect(result).toBeInstanceOf(TwoFactorDuoUpdateResponse); + expect(result.duo.enabled).toBe(true); + expect(result.duo.host).toBeDefined(); + expect(result.duo.clientId).toBeDefined(); + expect(result.duo.clientSecret).toContain("******"); }); }); describe("putTwoFactorOrganizationDuo", () => { it("enables organization-level Duo with policy management permissions", async () => { const organizationId = "org-123"; - const request = new UpdateTwoFactorDuoRequest(); - request.host = "api-xyz789.duosecurity.com"; - request.clientId = "DI4XYZ9MNOP3QRS"; - request.clientSecret = "orgcli-secret-value-here"; + const request = new TwoFactorDuoUpdateRequest( + "DI4XYZ9MNOP3QRS", + "orgcli-secret-value-here", + "api-xyz789.duosecurity.com", + "", + ); const mockResponse = { - Enabled: true, - Host: "api-xyz789.duosecurity.com", - ClientId: "DI4XYZ9MNOP3QRS", - ClientSecret: "orgcli******", + Duo: { + Enabled: true, + Host: "api-xyz789.duosecurity.com", + ClientId: "DI4XYZ9MNOP3QRS", + ClientSecret: "orgcli******", + }, }; apiService.send.mockResolvedValue(mockResponse); @@ -364,11 +396,11 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorDuoResponse); - expect(result.enabled).toBe(true); - expect(result.host).toBeDefined(); - expect(result.clientId).toBeDefined(); - expect(result.clientSecret).toContain("******"); + expect(result).toBeInstanceOf(TwoFactorOrganizationDuoUpdateResponse); + expect(result.duo.enabled).toBe(true); + expect(result.duo.host).toBeDefined(); + expect(result.duo.clientId).toBeDefined(); + expect(result.duo.clientSecret).toContain("******"); }); }); }); @@ -379,9 +411,12 @@ describe("TwoFactorApiService", () => { const request = new SecretVerificationRequest(); request.masterPasswordHash = "master-password-hash"; const mockResponse = { - Enabled: true, - Key1: "cccccccccccc", - Key2: "dddddddddddd", + YubiKey: { + Enabled: true, + Key1: "cccccccccccc", + Key2: "dddddddddddd", + }, + UserVerificationToken: "uv-token", }; apiService.send.mockResolvedValue(mockResponse); @@ -395,22 +430,31 @@ describe("TwoFactorApiService", () => { true, ); expect(result).toBeInstanceOf(TwoFactorYubiKeyResponse); - expect(result.enabled).toBe(true); - expect(result.key1).toBeDefined(); - expect(result.key2).toBeDefined(); + expect(result.yubiKey.enabled).toBe(true); + expect(result.yubiKey.key1).toBe("cccccccccccc"); + expect(result.yubiKey.key2).toBe("dddddddddddd"); + expect(result.userVerificationToken).toBe("uv-token"); }); }); describe("putTwoFactorYubiKey", () => { it("enables YubiKey two-factor for premium user after validating device OTPs", async () => { - const request = new UpdateTwoFactorYubikeyOtpRequest(); - request.key1 = "ccccccccccccjkhbhbhrkcitringjkrjirfjuunlnlvcghnkrtgfj"; - request.key2 = "ddddddddddddvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"; + const request = new TwoFactorYubiKeyUpdateRequest( + "ccccccccccccjkhbhbhrkcitringjkrjirfjuunlnlvcghnkrtgfj", + "ddddddddddddvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv", + "", + "", + "", + false, + "", + ); const mockResponse = { - Enabled: true, - Key1: "cccccccccccc", - Key2: "dddddddddddd", - Nfc: false, + YubiKey: { + Enabled: true, + Key1: "cccccccccccc", + Key2: "dddddddddddd", + Nfc: false, + }, }; apiService.send.mockResolvedValue(mockResponse); @@ -423,10 +467,10 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorYubiKeyResponse); - expect(result.enabled).toBe(true); - expect(result.key1).toBeDefined(); - expect(result.key2).toBeDefined(); + expect(result).toBeInstanceOf(TwoFactorYubiKeyUpdateResponse); + expect(result.yubiKey.enabled).toBe(true); + expect(result.yubiKey.key1).toBeDefined(); + expect(result.yubiKey.key2).toBeDefined(); }); }); }); @@ -437,11 +481,14 @@ describe("TwoFactorApiService", () => { const request = new SecretVerificationRequest(); request.masterPasswordHash = "master-password-hash"; const mockResponse = { - Enabled: true, - Keys: [ - { Name: "YubiKey 5", Id: 1, Migrated: false }, - { Name: "Security Key", Id: 2, Migrated: true }, - ], + WebAuthn: { + Enabled: true, + Keys: [ + { Name: "YubiKey 5", Id: 1, Migrated: false }, + { Name: "Security Key", Id: 2, Migrated: true }, + ], + }, + UserVerificationToken: "uv-token", }; apiService.send.mockResolvedValue(mockResponse); @@ -455,31 +502,33 @@ describe("TwoFactorApiService", () => { true, ); expect(result).toBeInstanceOf(TwoFactorWebAuthnResponse); - expect(result.enabled).toBe(true); - expect(result.keys).toHaveLength(2); - result.keys.forEach((key) => { + expect(result.webAuthn.enabled).toBe(true); + expect(result.webAuthn.keys).toHaveLength(2); + result.webAuthn.keys.forEach((key) => { expect(key).toHaveProperty("name"); expect(key).toHaveProperty("id"); expect(key).toHaveProperty("migrated"); }); + expect(result.userVerificationToken).toBe("uv-token"); }); }); describe("getTwoFactorWebAuthnChallenge", () => { - it("obtains cryptographic challenge for WebAuthn credential registration", async () => { - const request = new SecretVerificationRequest(); - request.masterPasswordHash = "master-password-hash"; + it("replays the cached user-verification token and obtains the wrapped challenge", async () => { + const request = new TwoFactorWebAuthnChallengeRequest("uv-token"); const mockResponse = { - challenge: "Y2hhbGxlbmdlLXN0cmluZw", - rp: { name: "Bitwarden" }, - user: { - id: "dXNlci1pZA", - name: "user@example.com", - displayName: "User", + Options: { + challenge: "Y2hhbGxlbmdlLXN0cmluZw", + rp: { name: "Bitwarden" }, + user: { + id: "dXNlci1pZA", + name: "user@example.com", + displayName: "User", + }, + pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256 + excludeCredentials: [] as PublicKeyCredentialDescriptor[], + timeout: 60000, }, - pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256 - excludeCredentials: [] as PublicKeyCredentialDescriptor[], - timeout: 60000, }; apiService.send.mockResolvedValue(mockResponse); @@ -492,14 +541,10 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(ChallengeResponse); - expect(result.challenge).toBeDefined(); - expect(result.rp).toHaveProperty("name", "Bitwarden"); - expect(result.user).toHaveProperty("id"); - expect(result.user).toHaveProperty("name"); - expect(result.user).toHaveProperty("displayName", "User"); - expect(result.pubKeyCredParams).toHaveLength(1); - expect(Number(result.timeout)).toBeTruthy(); + expect(result).toBeInstanceOf(TwoFactorWebAuthnChallengeResponse); + expect(result.options).toBeDefined(); + expect(result.options.challenge).toBeDefined(); + expect(result.options.rp).toHaveProperty("name", "Bitwarden"); }); }); @@ -517,13 +562,18 @@ describe("TwoFactorApiService", () => { getClientExtensionResults: jest.fn().mockReturnValue({}), }; - const request = new UpdateTwoFactorWebAuthnRequest(); - request.deviceResponse = mockCredential as PublicKeyCredential; - request.name = "My Security Key"; + const request = new TwoFactorWebAuthnUpdateRequest( + mockCredential as PublicKeyCredential, + "My Security Key", + 0, + "", + ); const mockResponse = { - Enabled: true, - Keys: [{ Name: "My Security Key", Id: 1, Migrated: false }], + WebAuthn: { + Enabled: true, + Keys: [{ Name: "My Security Key", Id: 1, Migrated: false }], + }, }; apiService.send.mockResolvedValue(mockResponse); @@ -548,12 +598,12 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorWebAuthnResponse); - expect(result.enabled).toBe(true); - expect(result.keys).toHaveLength(1); - expect(result.keys[0].name).toBeDefined(); - expect(result.keys[0].id).toBeDefined(); - expect(result.keys[0].migrated).toBeDefined(); + expect(result).toBeInstanceOf(TwoFactorWebAuthnUpdateResponse); + expect(result.webAuthn.enabled).toBe(true); + expect(result.webAuthn.keys).toHaveLength(1); + expect(result.webAuthn.keys[0].name).toBeDefined(); + expect(result.webAuthn.keys[0].id).toBeDefined(); + expect(result.webAuthn.keys[0].migrated).toBeDefined(); }); it("preserves original request object without mutation during serialization", async () => { @@ -569,12 +619,15 @@ describe("TwoFactorApiService", () => { getClientExtensionResults: jest.fn().mockReturnValue({}), }; - const request = new UpdateTwoFactorWebAuthnRequest(); - request.deviceResponse = mockCredential as PublicKeyCredential; - request.name = "My Security Key"; + const request = new TwoFactorWebAuthnUpdateRequest( + mockCredential as PublicKeyCredential, + "My Security Key", + 0, + "", + ); const originalDeviceResponse = request.deviceResponse; - apiService.send.mockResolvedValue({ enabled: true, keys: [] }); + apiService.send.mockResolvedValue({ WebAuthn: { Enabled: true, Keys: [] } }); await twoFactorApiService.putTwoFactorWebAuthn(request); @@ -586,12 +639,12 @@ describe("TwoFactorApiService", () => { describe("deleteTwoFactorWebAuthn", () => { it("removes specific WebAuthn credential while preserving other registered keys", async () => { - const request = new UpdateTwoFactorWebAuthnDeleteRequest(); - request.id = 1; - request.masterPasswordHash = "master-password-hash"; + const request = new TwoFactorWebAuthnDeleteRequest(1, "uv-token"); const mockResponse = { - Enabled: true, - Keys: [{ Name: "Security Key", Id: 2, Migrated: true }], // Key with id:1 removed + WebAuthn: { + Enabled: true, + Keys: [{ Name: "Security Key", Id: 2, Migrated: true }], // Key with id:1 removed + }, }; apiService.send.mockResolvedValue(mockResponse); @@ -604,9 +657,9 @@ describe("TwoFactorApiService", () => { true, true, ); - expect(result).toBeInstanceOf(TwoFactorWebAuthnResponse); - expect(result.keys).toHaveLength(1); - expect(result.keys[0].id).toBe(2); + expect(result).toBeInstanceOf(TwoFactorWebAuthnDeleteResponse); + expect(result.webAuthn.keys).toHaveLength(1); + expect(result.webAuthn.keys[0].id).toBe(2); }); }); }); @@ -637,60 +690,85 @@ describe("TwoFactorApiService", () => { }); }); - describe("Disable APIs", () => { - describe("putTwoFactorDisable", () => { - it("disables specified two-factor provider for current user", async () => { - const request = new TwoFactorProviderRequest(); - request.type = 0; // Authenticator - request.masterPasswordHash = "master-password-hash"; - const mockResponse = { - Enabled: false, - Type: 0, - }; - apiService.send.mockResolvedValue(mockResponse); + describe("Per-provider Delete APIs", () => { + describe("deleteTwoFactorYubiKey", () => { + it("removes YubiKey two-factor enrollment for the current user and expects no body", async () => { + const request = new TwoFactorYubiKeyDeleteRequest("uv-token"); - const result = await twoFactorApiService.putTwoFactorDisable(request); + await twoFactorApiService.deleteTwoFactorYubiKey(request); expect(apiService.send).toHaveBeenCalledWith( - "PUT", - "/two-factor/disable", + "DELETE", + "/two-factor/yubikey", request, true, + false, + ); + }); + }); + + describe("deleteTwoFactorDuo", () => { + it("removes Duo two-factor enrollment for the current user and expects no body", async () => { + const request = new TwoFactorDuoDeleteRequest("uv-token"); + + await twoFactorApiService.deleteTwoFactorDuo(request); + + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + "/two-factor/duo", + request, true, + false, ); - expect(result).toBeInstanceOf(TwoFactorProviderResponse); - expect(result.enabled).toBe(false); - expect(result.type).toBe(0); // Authenticator }); }); - describe("putTwoFactorOrganizationDisable", () => { - it("disables two-factor provider for organization with policy management permissions", async () => { - const organizationId = "org-123"; - const request = new TwoFactorProviderRequest(); - request.type = 6; // Duo - request.masterPasswordHash = "master-password-hash"; - const mockResponse = { - Enabled: false, - Type: 6, - }; - apiService.send.mockResolvedValue(mockResponse); + describe("deleteTwoFactorEmail", () => { + it("removes email two-factor enrollment for the current user and expects no body", async () => { + const request = new TwoFactorEmailDeleteRequest("uv-token"); - const result = await twoFactorApiService.putTwoFactorOrganizationDisable( - organizationId, + await twoFactorApiService.deleteTwoFactorEmail(request); + + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + "/two-factor/email", request, + true, + false, ); + }); + }); + + describe("deleteTwoFactorOrganizationDuo", () => { + it("removes Duo two-factor enrollment for an organization and expects no body", async () => { + const organizationId = "org-123"; + const request = new TwoFactorOrganizationDuoDeleteRequest("uv-token"); + + await twoFactorApiService.deleteTwoFactorOrganizationDuo(organizationId, request); expect(apiService.send).toHaveBeenCalledWith( - "PUT", - `/organizations/${organizationId}/two-factor/disable`, + "DELETE", + `/organizations/${organizationId}/two-factor/duo`, request, true, + false, + ); + }); + }); + + describe("deleteTwoFactorWebAuthnAll", () => { + it("removes the entire WebAuthn enrollment in a single round-trip and expects no body", async () => { + const request = new TwoFactorWebAuthnDeleteAllRequest("uv-token"); + + await twoFactorApiService.deleteTwoFactorWebAuthnAll(request); + + expect(apiService.send).toHaveBeenCalledWith( + "DELETE", + "/two-factor/webauthn/all", + request, true, + false, ); - expect(result).toBeInstanceOf(TwoFactorProviderResponse); - expect(result.enabled).toBe(false); - expect(result.type).toBe(6); // Duo }); }); }); diff --git a/libs/common/src/auth/two-factor/services/default-two-factor-api.service.ts b/libs/common/src/auth/two-factor/services/default-two-factor-api.service.ts index a124716fc33d..1afb86379f60 100644 --- a/libs/common/src/auth/two-factor/services/default-two-factor-api.service.ts +++ b/libs/common/src/auth/two-factor/services/default-two-factor-api.service.ts @@ -1,27 +1,39 @@ import { ApiService } from "../../../abstractions/api.service"; import { ListResponse } from "../../../models/response/list.response"; import { Utils } from "../../../platform/misc/utils"; -import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; -import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request"; -import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request"; -import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request"; -import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request"; -import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request"; -import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request"; -import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request"; -import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request"; -import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response"; -import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response"; -import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response"; -import { - TwoFactorWebAuthnResponse, - ChallengeResponse, -} from "../../models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response"; import { TwoFactorApiService } from "../abstractions/two-factor-api.service"; +import { TwoFactorAuthenticatorDeleteRequest } from "../request/two-factor-authenticator-delete.request"; +import { TwoFactorAuthenticatorUpdateRequest } from "../request/two-factor-authenticator-update.request"; +import { TwoFactorDuoDeleteRequest } from "../request/two-factor-duo-delete.request"; +import { TwoFactorDuoUpdateRequest } from "../request/two-factor-duo-update.request"; +import { TwoFactorEmailDeleteRequest } from "../request/two-factor-email-delete.request"; +import { TwoFactorEmailLoginRequest } from "../request/two-factor-email-login.request"; +import { TwoFactorEmailSetupRequest } from "../request/two-factor-email-setup.request"; +import { TwoFactorEmailUpdateRequest } from "../request/two-factor-email-update.request"; +import { TwoFactorOrganizationDuoDeleteRequest } from "../request/two-factor-organization-duo-delete.request"; +import { TwoFactorWebAuthnChallengeRequest } from "../request/two-factor-web-authn-challenge.request"; +import { TwoFactorWebAuthnDeleteAllRequest } from "../request/two-factor-web-authn-delete-all.request"; +import { TwoFactorWebAuthnDeleteRequest } from "../request/two-factor-web-authn-delete.request"; +import { TwoFactorWebAuthnUpdateRequest } from "../request/two-factor-web-authn-update.request"; +import { TwoFactorYubiKeyDeleteRequest } from "../request/two-factor-yubikey-delete.request"; +import { TwoFactorYubiKeyUpdateRequest } from "../request/two-factor-yubikey-update.request"; +import { TwoFactorAuthenticatorUpdateResponse } from "../response/two-factor-authenticator-update.response"; +import { TwoFactorAuthenticatorResponse } from "../response/two-factor-authenticator.response"; +import { TwoFactorDuoUpdateResponse } from "../response/two-factor-duo-update.response"; +import { TwoFactorDuoResponse } from "../response/two-factor-duo.response"; +import { TwoFactorEmailUpdateResponse } from "../response/two-factor-email-update.response"; +import { TwoFactorEmailResponse } from "../response/two-factor-email.response"; +import { TwoFactorOrganizationDuoUpdateResponse } from "../response/two-factor-organization-duo-update.response"; +import { TwoFactorOrganizationDuoResponse } from "../response/two-factor-organization-duo.response"; +import { TwoFactorProviderResponse } from "../response/two-factor-provider.response"; +import { TwoFactorRecoverResponse } from "../response/two-factor-recover.response"; +import { TwoFactorWebAuthnChallengeResponse } from "../response/two-factor-web-authn-challenge.response"; +import { TwoFactorWebAuthnDeleteResponse } from "../response/two-factor-web-authn-delete.response"; +import { TwoFactorWebAuthnUpdateResponse } from "../response/two-factor-web-authn-update.response"; +import { TwoFactorWebAuthnResponse } from "../response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyUpdateResponse } from "../response/two-factor-yubi-key-update.response"; +import { TwoFactorYubiKeyResponse } from "../response/two-factor-yubi-key.response"; export class DefaultTwoFactorApiService implements TwoFactorApiService { constructor(private apiService: ApiService) {} @@ -62,8 +74,8 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { } async putTwoFactorAuthenticator( - request: UpdateTwoFactorAuthenticatorRequest, - ): Promise { + request: TwoFactorAuthenticatorUpdateRequest, + ): Promise { const response = await this.apiService.send( "PUT", "/two-factor/authenticator", @@ -71,20 +83,11 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { true, true, ); - return new TwoFactorAuthenticatorResponse(response); + return new TwoFactorAuthenticatorUpdateResponse(response); } - async deleteTwoFactorAuthenticator( - request: DisableTwoFactorAuthenticatorRequest, - ): Promise { - const response = await this.apiService.send( - "DELETE", - "/two-factor/authenticator", - request, - true, - true, - ); - return new TwoFactorProviderResponse(response); + async deleteTwoFactorAuthenticator(request: TwoFactorAuthenticatorDeleteRequest): Promise { + await this.apiService.send("DELETE", "/two-factor/authenticator", request, true, false); } // Email @@ -100,17 +103,23 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { return new TwoFactorEmailResponse(response); } - async postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise { + async postTwoFactorEmailSetup(request: TwoFactorEmailSetupRequest): Promise { return this.apiService.send("POST", "/two-factor/send-email", request, true, false); } - async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + async postTwoFactorEmail(request: TwoFactorEmailLoginRequest): Promise { return this.apiService.send("POST", "/two-factor/send-email-login", request, false, false); } - async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise { + async putTwoFactorEmail( + request: TwoFactorEmailUpdateRequest, + ): Promise { const response = await this.apiService.send("PUT", "/two-factor/email", request, true, true); - return new TwoFactorEmailResponse(response); + return new TwoFactorEmailUpdateResponse(response); + } + + async deleteTwoFactorEmail(request: TwoFactorEmailDeleteRequest): Promise { + await this.apiService.send("DELETE", "/two-factor/email", request, true, false); } // Duo @@ -123,7 +132,7 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { async getTwoFactorOrganizationDuo( organizationId: string, request: SecretVerificationRequest, - ): Promise { + ): Promise { const response = await this.apiService.send( "POST", `/organizations/${organizationId}/two-factor/get-duo`, @@ -131,18 +140,22 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { true, true, ); - return new TwoFactorDuoResponse(response); + return new TwoFactorOrganizationDuoResponse(response); } - async putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise { + async putTwoFactorDuo(request: TwoFactorDuoUpdateRequest): Promise { const response = await this.apiService.send("PUT", "/two-factor/duo", request, true, true); - return new TwoFactorDuoResponse(response); + return new TwoFactorDuoUpdateResponse(response); + } + + async deleteTwoFactorDuo(request: TwoFactorDuoDeleteRequest): Promise { + await this.apiService.send("DELETE", "/two-factor/duo", request, true, false); } async putTwoFactorOrganizationDuo( organizationId: string, - request: UpdateTwoFactorDuoRequest, - ): Promise { + request: TwoFactorDuoUpdateRequest, + ): Promise { const response = await this.apiService.send( "PUT", `/organizations/${organizationId}/two-factor/duo`, @@ -150,7 +163,20 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { true, true, ); - return new TwoFactorDuoResponse(response); + return new TwoFactorOrganizationDuoUpdateResponse(response); + } + + async deleteTwoFactorOrganizationDuo( + organizationId: string, + request: TwoFactorOrganizationDuoDeleteRequest, + ): Promise { + await this.apiService.send( + "DELETE", + `/organizations/${organizationId}/two-factor/duo`, + request, + true, + false, + ); } // YubiKey @@ -167,10 +193,14 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { } async putTwoFactorYubiKey( - request: UpdateTwoFactorYubikeyOtpRequest, - ): Promise { + request: TwoFactorYubiKeyUpdateRequest, + ): Promise { const response = await this.apiService.send("PUT", "/two-factor/yubikey", request, true, true); - return new TwoFactorYubiKeyResponse(response); + return new TwoFactorYubiKeyUpdateResponse(response); + } + + async deleteTwoFactorYubiKey(request: TwoFactorYubiKeyDeleteRequest): Promise { + await this.apiService.send("DELETE", "/two-factor/yubikey", request, true, false); } // WebAuthn @@ -189,8 +219,8 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { } async getTwoFactorWebAuthnChallenge( - request: SecretVerificationRequest, - ): Promise { + request: TwoFactorWebAuthnChallengeRequest, + ): Promise { const response = await this.apiService.send( "POST", "/two-factor/get-webauthn-challenge", @@ -198,12 +228,12 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { true, true, ); - return new ChallengeResponse(response); + return new TwoFactorWebAuthnChallengeResponse(response); } async putTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnRequest, - ): Promise { + request: TwoFactorWebAuthnUpdateRequest, + ): Promise { const deviceResponse = request.deviceResponse.response as AuthenticatorAttestationResponse; const body: any = Object.assign({}, request); @@ -219,12 +249,12 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { }; const response = await this.apiService.send("PUT", "/two-factor/webauthn", body, true, true); - return new TwoFactorWebAuthnResponse(response); + return new TwoFactorWebAuthnUpdateResponse(response); } async deleteTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnDeleteRequest, - ): Promise { + request: TwoFactorWebAuthnDeleteRequest, + ): Promise { const response = await this.apiService.send( "DELETE", "/two-factor/webauthn", @@ -232,7 +262,11 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { true, true, ); - return new TwoFactorWebAuthnResponse(response); + return new TwoFactorWebAuthnDeleteResponse(response); + } + + async deleteTwoFactorWebAuthnAll(request: TwoFactorWebAuthnDeleteAllRequest): Promise { + await this.apiService.send("DELETE", "/two-factor/webauthn/all", request, true, false); } // Recovery Code @@ -247,25 +281,4 @@ export class DefaultTwoFactorApiService implements TwoFactorApiService { ); return new TwoFactorRecoverResponse(response); } - - // Disable - - async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise { - const response = await this.apiService.send("PUT", "/two-factor/disable", request, true, true); - return new TwoFactorProviderResponse(response); - } - - async putTwoFactorOrganizationDisable( - organizationId: string, - request: TwoFactorProviderRequest, - ): Promise { - const response = await this.apiService.send( - "PUT", - `/organizations/${organizationId}/two-factor/disable`, - request, - true, - true, - ); - return new TwoFactorProviderResponse(response); - } } diff --git a/libs/common/src/auth/two-factor/services/default-two-factor.service.ts b/libs/common/src/auth/two-factor/services/default-two-factor.service.ts index 2acc4a7e9399..e4814a1c383f 100644 --- a/libs/common/src/auth/two-factor/services/default-two-factor.service.ts +++ b/libs/common/src/auth/two-factor/services/default-two-factor.service.ts @@ -9,27 +9,8 @@ import { PlatformUtilsService } from "../../../platform/abstractions/platform-ut import { Utils } from "../../../platform/misc/utils"; import { GlobalStateProvider } from "../../../platform/state"; import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; -import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request"; import { SecretVerificationRequest } from "../../models/request/secret-verification.request"; -import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request"; -import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request"; -import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request"; -import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request"; -import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request"; -import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request"; -import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request"; -import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request"; import { IdentityTwoFactorResponse } from "../../models/response/identity-two-factor.response"; -import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response"; -import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response"; -import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response"; -import { - TwoFactorWebAuthnResponse, - ChallengeResponse, -} from "../../models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response"; import { PROVIDERS, SELECTED_PROVIDER, @@ -37,6 +18,37 @@ import { TwoFactorProviders, TwoFactorService as TwoFactorServiceAbstraction, } from "../abstractions/two-factor.service"; +import { TwoFactorAuthenticatorDeleteRequest } from "../request/two-factor-authenticator-delete.request"; +import { TwoFactorAuthenticatorUpdateRequest } from "../request/two-factor-authenticator-update.request"; +import { TwoFactorDuoDeleteRequest } from "../request/two-factor-duo-delete.request"; +import { TwoFactorDuoUpdateRequest } from "../request/two-factor-duo-update.request"; +import { TwoFactorEmailDeleteRequest } from "../request/two-factor-email-delete.request"; +import { TwoFactorEmailLoginRequest } from "../request/two-factor-email-login.request"; +import { TwoFactorEmailSetupRequest } from "../request/two-factor-email-setup.request"; +import { TwoFactorEmailUpdateRequest } from "../request/two-factor-email-update.request"; +import { TwoFactorOrganizationDuoDeleteRequest } from "../request/two-factor-organization-duo-delete.request"; +import { TwoFactorWebAuthnChallengeRequest } from "../request/two-factor-web-authn-challenge.request"; +import { TwoFactorWebAuthnDeleteAllRequest } from "../request/two-factor-web-authn-delete-all.request"; +import { TwoFactorWebAuthnDeleteRequest } from "../request/two-factor-web-authn-delete.request"; +import { TwoFactorWebAuthnUpdateRequest } from "../request/two-factor-web-authn-update.request"; +import { TwoFactorYubiKeyDeleteRequest } from "../request/two-factor-yubikey-delete.request"; +import { TwoFactorYubiKeyUpdateRequest } from "../request/two-factor-yubikey-update.request"; +import { TwoFactorAuthenticatorUpdateResponse } from "../response/two-factor-authenticator-update.response"; +import { TwoFactorAuthenticatorResponse } from "../response/two-factor-authenticator.response"; +import { TwoFactorDuoUpdateResponse } from "../response/two-factor-duo-update.response"; +import { TwoFactorDuoResponse } from "../response/two-factor-duo.response"; +import { TwoFactorEmailUpdateResponse } from "../response/two-factor-email-update.response"; +import { TwoFactorEmailResponse } from "../response/two-factor-email.response"; +import { TwoFactorOrganizationDuoUpdateResponse } from "../response/two-factor-organization-duo-update.response"; +import { TwoFactorOrganizationDuoResponse } from "../response/two-factor-organization-duo.response"; +import { TwoFactorProviderResponse } from "../response/two-factor-provider.response"; +import { TwoFactorRecoverResponse } from "../response/two-factor-recover.response"; +import { TwoFactorWebAuthnChallengeResponse } from "../response/two-factor-web-authn-challenge.response"; +import { TwoFactorWebAuthnDeleteResponse } from "../response/two-factor-web-authn-delete.response"; +import { TwoFactorWebAuthnUpdateResponse } from "../response/two-factor-web-authn-update.response"; +import { TwoFactorWebAuthnResponse } from "../response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyUpdateResponse } from "../response/two-factor-yubi-key-update.response"; +import { TwoFactorYubiKeyResponse } from "../response/two-factor-yubi-key.response"; export class DefaultTwoFactorService implements TwoFactorServiceAbstraction { private providersState = this.globalStateProvider.get(PROVIDERS); @@ -193,7 +205,7 @@ export class DefaultTwoFactorService implements TwoFactorServiceAbstraction { getTwoFactorOrganizationDuo( organizationId: string, request: SecretVerificationRequest, - ): Promise { + ): Promise { return this.twoFactorApiService.getTwoFactorOrganizationDuo(organizationId, request); } @@ -205,7 +217,9 @@ export class DefaultTwoFactorService implements TwoFactorServiceAbstraction { return this.twoFactorApiService.getTwoFactorWebAuthn(request); } - getTwoFactorWebAuthnChallenge(request: SecretVerificationRequest): Promise { + getTwoFactorWebAuthnChallenge( + request: TwoFactorWebAuthnChallengeRequest, + ): Promise { return this.twoFactorApiService.getTwoFactorWebAuthnChallenge(request); } @@ -214,66 +228,76 @@ export class DefaultTwoFactorService implements TwoFactorServiceAbstraction { } putTwoFactorAuthenticator( - request: UpdateTwoFactorAuthenticatorRequest, - ): Promise { + request: TwoFactorAuthenticatorUpdateRequest, + ): Promise { return this.twoFactorApiService.putTwoFactorAuthenticator(request); } - deleteTwoFactorAuthenticator( - request: DisableTwoFactorAuthenticatorRequest, - ): Promise { + deleteTwoFactorAuthenticator(request: TwoFactorAuthenticatorDeleteRequest): Promise { return this.twoFactorApiService.deleteTwoFactorAuthenticator(request); } - putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise { + putTwoFactorEmail(request: TwoFactorEmailUpdateRequest): Promise { return this.twoFactorApiService.putTwoFactorEmail(request); } - putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise { + putTwoFactorDuo(request: TwoFactorDuoUpdateRequest): Promise { return this.twoFactorApiService.putTwoFactorDuo(request); } putTwoFactorOrganizationDuo( organizationId: string, - request: UpdateTwoFactorDuoRequest, - ): Promise { + request: TwoFactorDuoUpdateRequest, + ): Promise { return this.twoFactorApiService.putTwoFactorOrganizationDuo(organizationId, request); } putTwoFactorYubiKey( - request: UpdateTwoFactorYubikeyOtpRequest, - ): Promise { + request: TwoFactorYubiKeyUpdateRequest, + ): Promise { return this.twoFactorApiService.putTwoFactorYubiKey(request); } putTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnRequest, - ): Promise { + request: TwoFactorWebAuthnUpdateRequest, + ): Promise { return this.twoFactorApiService.putTwoFactorWebAuthn(request); } deleteTwoFactorWebAuthn( - request: UpdateTwoFactorWebAuthnDeleteRequest, - ): Promise { + request: TwoFactorWebAuthnDeleteRequest, + ): Promise { return this.twoFactorApiService.deleteTwoFactorWebAuthn(request); } - putTwoFactorDisable(request: TwoFactorProviderRequest): Promise { - return this.twoFactorApiService.putTwoFactorDisable(request); + deleteTwoFactorYubiKey(request: TwoFactorYubiKeyDeleteRequest): Promise { + return this.twoFactorApiService.deleteTwoFactorYubiKey(request); + } + + deleteTwoFactorDuo(request: TwoFactorDuoDeleteRequest): Promise { + return this.twoFactorApiService.deleteTwoFactorDuo(request); } - putTwoFactorOrganizationDisable( + deleteTwoFactorEmail(request: TwoFactorEmailDeleteRequest): Promise { + return this.twoFactorApiService.deleteTwoFactorEmail(request); + } + + deleteTwoFactorOrganizationDuo( organizationId: string, - request: TwoFactorProviderRequest, - ): Promise { - return this.twoFactorApiService.putTwoFactorOrganizationDisable(organizationId, request); + request: TwoFactorOrganizationDuoDeleteRequest, + ): Promise { + return this.twoFactorApiService.deleteTwoFactorOrganizationDuo(organizationId, request); + } + + deleteTwoFactorWebAuthnAll(request: TwoFactorWebAuthnDeleteAllRequest): Promise { + return this.twoFactorApiService.deleteTwoFactorWebAuthnAll(request); } - postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise { + postTwoFactorEmailSetup(request: TwoFactorEmailSetupRequest): Promise { return this.twoFactorApiService.postTwoFactorEmailSetup(request); } - postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + postTwoFactorEmail(request: TwoFactorEmailLoginRequest): Promise { return this.twoFactorApiService.postTwoFactorEmail(request); } } diff --git a/libs/common/src/auth/two-factor/types/index.ts b/libs/common/src/auth/two-factor/types/index.ts new file mode 100644 index 000000000000..09b330939524 --- /dev/null +++ b/libs/common/src/auth/two-factor/types/index.ts @@ -0,0 +1,3 @@ +export * from "./two-factor-response"; +export * from "./two-factor-setup-dialog-data"; +export * from "./two-factor-user-verification-result"; diff --git a/libs/common/src/auth/two-factor/types/two-factor-response.ts b/libs/common/src/auth/two-factor/types/two-factor-response.ts new file mode 100644 index 000000000000..d9b2a6542f41 --- /dev/null +++ b/libs/common/src/auth/two-factor/types/two-factor-response.ts @@ -0,0 +1,16 @@ +import { TwoFactorAuthenticatorResponse } from "../response/two-factor-authenticator.response"; +import { TwoFactorDuoResponse } from "../response/two-factor-duo.response"; +import { TwoFactorEmailResponse } from "../response/two-factor-email.response"; +import { TwoFactorOrganizationDuoResponse } from "../response/two-factor-organization-duo.response"; +import { TwoFactorRecoverResponse } from "../response/two-factor-recover.response"; +import { TwoFactorWebAuthnResponse } from "../response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyResponse } from "../response/two-factor-yubi-key.response"; + +export type TwoFactorResponse = + | TwoFactorRecoverResponse + | TwoFactorDuoResponse + | TwoFactorOrganizationDuoResponse + | TwoFactorEmailResponse + | TwoFactorWebAuthnResponse + | TwoFactorAuthenticatorResponse + | TwoFactorYubiKeyResponse; diff --git a/libs/common/src/auth/two-factor/types/two-factor-setup-dialog-data.ts b/libs/common/src/auth/two-factor/types/two-factor-setup-dialog-data.ts new file mode 100644 index 000000000000..f217423de196 --- /dev/null +++ b/libs/common/src/auth/two-factor/types/two-factor-setup-dialog-data.ts @@ -0,0 +1,13 @@ +import { TwoFactorResponse } from "./two-factor-response"; +import { TwoFactorUserVerificationResult } from "./two-factor-user-verification-result"; + +/** + * Return type of `TwoFactorVerifyDialogComponent`. Bundles the user-verification proof + * with the provider's current server state fetched alongside the verification step, + * and is then passed as `DIALOG_DATA` into the per-provider 2FA setup dialogs + * (authenticator, email, yubikey, webauthn, duo). + */ +export type TwoFactorSetupDialogData = + TwoFactorUserVerificationResult & { + response: T; + }; diff --git a/libs/common/src/auth/two-factor/types/two-factor-user-verification-result.ts b/libs/common/src/auth/two-factor/types/two-factor-user-verification-result.ts new file mode 100644 index 000000000000..12d8a97c512c --- /dev/null +++ b/libs/common/src/auth/two-factor/types/two-factor-user-verification-result.ts @@ -0,0 +1,13 @@ +import { VerificationType } from "../../enums/verification-type"; + +/** + * Proof that the user re-authenticated (via master password / OTP) before managing 2FA. + * Consumed by `UserVerificationService.buildRequest` to construct the `SecretVerificationRequest` + * sent to the per-provider GET endpoint (or the WebAuthn challenge POST) that mints the + * user-verification token. Subsequent per-provider PUT/DELETE calls thread that token + * directly, not this result. + */ +export type TwoFactorUserVerificationResult = { + secret: string; + verificationType: VerificationType; +}; diff --git a/libs/common/src/auth/types/auth-response.ts b/libs/common/src/auth/types/auth-response.ts deleted file mode 100644 index b5fad923de6a..000000000000 --- a/libs/common/src/auth/types/auth-response.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { VerificationType } from "../enums/verification-type"; - -import { TwoFactorResponse } from "./two-factor-response"; - -export type AuthResponseBase = { - secret: string; - verificationType: VerificationType; -}; - -export type AuthResponse = AuthResponseBase & { - response: T; -}; diff --git a/libs/common/src/auth/types/two-factor-response.ts b/libs/common/src/auth/types/two-factor-response.ts deleted file mode 100644 index 2b2875b1bf96..000000000000 --- a/libs/common/src/auth/types/two-factor-response.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TwoFactorAuthenticatorResponse } from "../models/response/two-factor-authenticator.response"; -import { TwoFactorDuoResponse } from "../models/response/two-factor-duo.response"; -import { TwoFactorEmailResponse } from "../models/response/two-factor-email.response"; -import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response"; -import { TwoFactorWebAuthnResponse } from "../models/response/two-factor-web-authn.response"; -import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response"; - -export type TwoFactorResponse = - | TwoFactorRecoverResponse - | TwoFactorDuoResponse - | TwoFactorEmailResponse - | TwoFactorWebAuthnResponse - | TwoFactorAuthenticatorResponse - | TwoFactorYubiKeyResponse;