diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index f4604ba68a9..80e6d44abb2 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -43,7 +43,7 @@ import { UserLockIcon, VaultIcon, } from "@bitwarden/auth/angular"; -import { LockComponent } from "@bitwarden/key-management-ui"; +import { ConfirmKeyConnectorDomainComponent, LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, @@ -225,6 +225,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, + { + path: "confirm-key-connector-domain", + component: ConfirmKeyConnectorDomainComponent, + canActivate: [authGuard], + data: { elevation: 1 } satisfies RouteDataProperties, + }, { path: "view-cipher", component: ViewV2Component, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 1b70168dcae..701b431ff57 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -42,7 +42,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; -import { LockComponent } from "@bitwarden/key-management-ui"; +import { ConfirmKeyConnectorDomainComponent, LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, @@ -178,6 +178,11 @@ const routes: Routes = [ component: RemovePasswordComponent, canActivate: [authGuard], }, + { + path: "confirm-key-connector-domain", + component: ConfirmKeyConnectorDomainComponent, + canActivate: [authGuard], + }, { path: "passkeys", component: Fido2PlaceholderComponent, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 7cf16e55bed..bb7ec4a8066 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -41,7 +41,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; -import { LockComponent } from "@bitwarden/key-management-ui"; +import { ConfirmKeyConnectorDomainComponent, LockComponent } from "@bitwarden/key-management-ui"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, @@ -570,6 +570,17 @@ const routes: Routes = [ titleId: "removeMasterPassword", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, + { + path: "confirm-key-connector-domain", + component: ConfirmKeyConnectorDomainComponent, + canActivate: [authGuard], + data: { + pageTitle: { + key: "confirmKeyConnectorDomain", + }, + titleId: "confirmKeyConnectorDomain", + } satisfies RouteDataProperties & AnonLayoutWrapperData, + }, { path: "trial-initiation", canActivate: [unauthGuardFn()], diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 24a619e28cf..8c19e1d7e11 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -27,8 +27,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; +import { KdfType } from "@bitwarden/key-management"; @Directive() export class SsoComponent implements OnInit { @@ -219,6 +221,13 @@ export class SsoComponent implements OnInit { return await this.handleTwoFactorRequired(orgSsoIdentifier); } + if (authResult.requiresKeyConnectorDomainConfirmation != null) { + return await this.handleKeyConnectorDomainConfirmation( + authResult.requiresKeyConnectorDomainConfirmation, + authResult.userId, + ); + } + // Everything after the 2FA check is considered a successful login // Just have to figure out where to send the user @@ -419,4 +428,22 @@ export class SsoComponent implements OnInit { const checkStateSplit = checkState.split("_identifier="); return stateSplit[0] === checkStateSplit[0]; } + + private async handleKeyConnectorDomainConfirmation( + request: { + kdf: KdfType; + kdfIterations: number; + kdfMemory?: number; + kdfParallelism?: number; + keyConnectorUrl: string; + }, + userId: UserId, + ) { + await this.router.navigate(["confirm-key-connector-domain"], { + state: { + ...request, + userId, + }, + }); + } } diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 3d91adf35cf..ffb9fe74494 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -77,7 +77,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy { return super.logInTwoFactor(twoFactor); } - protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { + protected override async setMasterKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { const authRequestCredentials = this.cache.value.authRequestCredentials; if ( authRequestCredentials.decryptedMasterKey && @@ -92,6 +95,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { userId, ); } + + return null; } protected override async setUserKey( diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 96d7b6b0f74..b1e227ca428 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -296,7 +296,12 @@ export abstract class LoginStrategy { await this.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken); } - await this.setMasterKey(response, userId); + const masterKeyResult = await this.setMasterKey(response, userId); + if (masterKeyResult != null) { + result.requiresKeyConnectorDomainConfirmation = + masterKeyResult.requiresKeyConnectorDomainConfirmation; + } + await this.setUserKey(response, userId); await this.setPrivateKey(response, userId); @@ -306,7 +311,19 @@ export abstract class LoginStrategy { } // The keys comes from different sources depending on the login strategy - protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise; + protected abstract setMasterKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise<{ + requiresKeyConnectorDomainConfirmation: { + kdf: KdfType; + kdfIterations: number; + kdfMemory?: number; + kdfParallelism?: number; + keyConnectorUrl: string; + organizationId: string; + }; + } | null>; protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise; diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index f0a8d40f914..50f53149969 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -168,10 +168,15 @@ export class PasswordLoginStrategy extends LoginStrategy { return result; } - protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { + protected override async setMasterKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { const { masterKey, localMasterKeyHash } = this.cache.value; await this.masterPasswordService.setMasterKey(masterKey, userId); await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); + + return null; } protected override async setUserKey( diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 1dd01d6fc75..76e01f6a101 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -125,16 +125,23 @@ export class SsoLoginStrategy extends LoginStrategy { // The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector. const newSsoUser = tokenResponse.key == null; if (newSsoUser) { - await this.keyConnectorService.convertNewSsoUserToKeyConnector( - tokenResponse, - this.cache.value.orgId, - userId, - ); + return { + requiresKeyConnectorDomainConfirmation: { + kdf: tokenResponse.kdf, + kdfIterations: tokenResponse.kdfIterations, + kdfMemory: tokenResponse.kdfMemory, + kdfParallelism: tokenResponse.kdfParallelism, + keyConnectorUrl: this.getKeyConnectorUrl(tokenResponse), + organizationId: this.cache.value.orgId, + }, + }; } else { const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse); await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId); } } + + return null; } /** @@ -211,7 +218,7 @@ export class SsoLoginStrategy extends LoginStrategy { this.getKeyConnectorUrl(tokenResponse) != null ) { // Key connector enabled for user - await this.trySetUserKeyWithMasterKey(userId); + await this.trySetUserKeyWithMasterKey(tokenResponse, userId); } // Note: In the traditional SSO flow with MP without key connector, the lock component @@ -321,7 +328,17 @@ export class SsoLoginStrategy extends LoginStrategy { } } - private async trySetUserKeyWithMasterKey(userId: UserId): Promise { + private async trySetUserKeyWithMasterKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise { + const newSsoUser = tokenResponse.key == null; + // For new users with Key Connector, we will not have a master key yet, since Key Connector + // domain have to be confirmed first. + if (newSsoUser) { + return; + } + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); // There is a scenario in which the master key is not set here. That will occur if the user diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 32cd5ceaf40..8308c8f686e 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -52,12 +52,16 @@ export class UserApiLoginStrategy extends LoginStrategy { return authResult; } - protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { + protected override async setMasterKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { if (response.apiUseKeyConnector) { const env = await firstValueFrom(this.environmentService.environment$); const keyConnectorUrl = env.getKeyConnectorUrl(); await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId); } + return null; } protected override async setUserKey( diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 97696a26699..75ad9ab0096 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -59,8 +59,11 @@ export class WebAuthnLoginStrategy extends LoginStrategy { throw new Error("2FA not supported yet for WebAuthn Login."); } - protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { - return Promise.resolve(); + protected override async setMasterKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { + return null; } protected override async setUserKey(idTokenResponse: IdentityTokenResponse, userId: UserId) { diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index fdc8c963a1b..f58cfd4a0b3 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { KdfType } from "@bitwarden/key-management"; + import { Utils } from "../../../platform/misc/utils"; import { UserId } from "../../../types/guid"; import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; @@ -23,6 +25,14 @@ export class AuthResult { email: string; requiresEncryptionKeyMigration: boolean; requiresDeviceVerification: boolean; + requiresKeyConnectorDomainConfirmation?: { + kdf: KdfType; + kdfIterations: number; + kdfMemory?: number; + kdfParallelism?: number; + keyConnectorUrl: string; + organizationId: string; + }; get requiresCaptcha() { return !Utils.isNullOrWhitespace(this.captchaSiteKey); diff --git a/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts b/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts index 131b941f274..2ce691f1c3c 100644 --- a/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/abstractions/key-connector.service.ts @@ -1,7 +1,8 @@ import { Observable } from "rxjs"; +import { KdfType } from "@bitwarden/key-management"; + import { Organization } from "../../../admin-console/models/domain/organization"; -import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { UserId } from "../../../types/guid"; export abstract class KeyConnectorService { @@ -14,9 +15,13 @@ export abstract class KeyConnectorService { abstract migrateUser(userId: UserId): Promise; abstract convertNewSsoUserToKeyConnector( - tokenResponse: IdentityTokenResponse, orgId: string, userId: UserId, + keyConnectorUrl: string, + kdf: KdfType, + kdfIterations: number, + kdfMemory?: number, + kdfParallelism?: number, ): Promise; abstract setUsesKeyConnector(enabled: boolean, userId: UserId): Promise; diff --git a/libs/common/src/key-management/key-connector/services/key-connector.service.ts b/libs/common/src/key-management/key-connector/services/key-connector.service.ts index 974c6b2ee17..d4b67c0300c 100644 --- a/libs/common/src/key-management/key-connector/services/key-connector.service.ts +++ b/libs/common/src/key-management/key-connector/services/key-connector.service.ts @@ -17,7 +17,6 @@ import { OrganizationService } from "../../../admin-console/abstractions/organiz import { OrganizationUserType } from "../../../admin-console/enums"; import { Organization } from "../../../admin-console/models/domain/organization"; import { TokenService } from "../../../auth/abstractions/token.service"; -import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { KeysRequest } from "../../../models/request/keys.request"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { LogService } from "../../../platform/abstractions/log.service"; @@ -133,19 +132,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async convertNewSsoUserToKeyConnector( - tokenResponse: IdentityTokenResponse, orgId: string, userId: UserId, + keyConnectorUrl: string, + kdf: KdfType, + kdfIterations: number, + kdfMemory?: number, + kdfParallelism?: number, ) { - // TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) - const { - kdf, - kdfIterations, - kdfMemory, - kdfParallelism, - keyConnectorUrl: legacyKeyConnectorUrl, - userDecryptionOptions, - } = tokenResponse; const password = await this.keyGenerationService.createKey(512); const kdfConfig: KdfConfig = kdf === KdfType.PBKDF2_SHA256 @@ -167,8 +161,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const [pubKey, privKey] = await this.keyService.makeKeyPair(userKey[0]); try { - const keyConnectorUrl = - legacyKeyConnectorUrl ?? userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl; await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest); } catch (e) { this.handleKeyConnectorError(e); diff --git a/libs/key-management-ui/src/index.ts b/libs/key-management-ui/src/index.ts index e74ec44525b..28b68f0e5c4 100644 --- a/libs/key-management-ui/src/index.ts +++ b/libs/key-management-ui/src/index.ts @@ -5,3 +5,4 @@ export { LockComponent } from "./lock/components/lock.component"; export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service"; export { RemovePasswordComponent } from "./key-connector/remove-password.component"; +export { ConfirmKeyConnectorDomainComponent } from "./key-connector/confirm-key-connector-domain.component"; diff --git a/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html b/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html new file mode 100644 index 00000000000..7c89b545c5a --- /dev/null +++ b/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.html @@ -0,0 +1 @@ +
diff --git a/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.ts b/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.ts new file mode 100644 index 00000000000..ef244015986 --- /dev/null +++ b/libs/key-management-ui/src/key-connector/confirm-key-connector-domain.component.ts @@ -0,0 +1,58 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Subject } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + IconButtonModule, +} from "@bitwarden/components"; + +@Component({ + selector: "confirm-key-connector-domain", + templateUrl: "confirm-key-connector-domain.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + ReactiveFormsModule, + ButtonModule, + FormFieldModule, + AsyncActionsModule, + IconButtonModule, + ], +}) +export class ConfirmKeyConnectorDomainComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + constructor( + private route: ActivatedRoute, + private router: Router, + private accountService: AccountService, + private keyConnectorService: KeyConnectorService, + ) { + // TODO + // this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { + // console.log("[confirm-key-connector-domain]: account", account); + // }); + } + + ngOnInit() { + throw new Error("Method not implemented."); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + confirm = async () => { + // this.keyConnectorService.convertNewSsoUserToKeyConnector(); + }; +}