1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 04:33:38 +00:00

show key connector domain for new sso users

This commit is contained in:
Maciej Zieniuk
2025-04-03 16:22:17 +01:00
parent a10cd49e53
commit 8538614d75
16 changed files with 199 additions and 32 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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()],

View File

@@ -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,
},
});
}
}

View File

@@ -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<null> {
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(

View File

@@ -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<void>;
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<void>;

View File

@@ -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<null> {
const { masterKey, localMasterKeyHash } = this.cache.value;
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
return null;
}
protected override async setUserKey(

View File

@@ -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<void> {
private async trySetUserKeyWithMasterKey(
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
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

View File

@@ -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<null> {
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(

View File

@@ -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<null> {
return null;
}
protected override async setUserKey(idTokenResponse: IdentityTokenResponse, userId: UserId) {

View File

@@ -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);

View File

@@ -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<void>;
abstract convertNewSsoUserToKeyConnector(
tokenResponse: IdentityTokenResponse,
orgId: string,
userId: UserId,
keyConnectorUrl: string,
kdf: KdfType,
kdfIterations: number,
kdfMemory?: number,
kdfParallelism?: number,
): Promise<void>;
abstract setUsesKeyConnector(enabled: boolean, userId: UserId): Promise<void>;

View File

@@ -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);

View File

@@ -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";

View File

@@ -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<void>();
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();
};
}