Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/src/service-container/service-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ export class ServiceContainer {
this.messagingService,
this.logService,
this.keyConnectorService,
this.unlockService,
this.environmentService,
this.stateService,
this.twoFactorService,
Expand Down
1 change: 1 addition & 0 deletions libs/angular/src/services/jslib-services.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ const safeProviders: SafeProvider[] = [
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
UnlockService,
EnvironmentService,
StateServiceAbstraction,
TwoFactorService,
Expand Down
2 changes: 1 addition & 1 deletion libs/auth/src/common/login-strategies/login.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,8 @@ export abstract class LoginStrategy {
}

await this.setMasterKey(response, userId);
await this.setUserKey(response, userId);
await this.setAccountCryptographicState(response, userId);
await this.setUserKey(response, userId);

// This needs to run after the keys are set because it checks for the existence of the encrypted private key
await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";
Expand All @@ -24,7 +25,6 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
Expand All @@ -36,6 +36,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { DeviceKey, MasterKey, UserKey } from "@bitwarden/common/types/key";
import { Argon2KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { UnlockService } from "@bitwarden/unlock";

import {
AuthRequestServiceAbstraction,
Expand Down Expand Up @@ -63,9 +64,9 @@ describe("SsoLoginStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let unlockService: MockProxy<UnlockService>;
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
let kdfConfigService: MockProxy<KdfConfigService>;
Expand Down Expand Up @@ -102,9 +103,9 @@ describe("SsoLoginStrategy", () => {
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>();
unlockService = mock<UnlockService>();
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>();
i18nService = mock<I18nService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
kdfConfigService = mock<KdfConfigService>();
Expand Down Expand Up @@ -144,9 +145,9 @@ describe("SsoLoginStrategy", () => {
ssoLoginStrategy = new SsoLoginStrategy(
{} as SsoLoginStrategyData,
keyConnectorService,
unlockService,
deviceTrustService,
authRequestService,
i18nService,
accountService,
masterPasswordService,
keyService,
Expand Down Expand Up @@ -487,6 +488,7 @@ describe("SsoLoginStrategy", () => {
HasMasterPassword: false,
KeyConnectorOption: { KeyConnectorUrl: keyConnectorUrl },
});
tokenResponse.apiUseKeyConnector = true;
});

it("gets and sets the master key if Key Connector is enabled and the user doesn't have a master password", async () => {
Expand All @@ -502,6 +504,21 @@ describe("SsoLoginStrategy", () => {
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
});

it("uses unlock service when SDK key connector feature flag is enabled", async () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
configService.getFeatureFlag
.calledWith(FeatureFlag.UnlockKeyConnectorWithSdk)
.mockResolvedValue(true);

await ssoLoginStrategy.logIn(credentials);

expect(unlockService.unlockWithKeyConnector).toHaveBeenCalledWith(userId, {
url: keyConnectorUrl,
keyConnectorKeyWrappedUserKey: tokenResponse.key!.encryptedString!,
});
expect(keyConnectorService.setMasterKeyFromUrl).not.toHaveBeenCalled();
});

it("converts new SSO user with no master password to Key Connector on first login", async () => {
tokenResponse.key = undefined;
tokenResponse.kdfConfig = new Argon2KdfConfig(10, 64, 4);
Expand Down
23 changes: 19 additions & 4 deletions libs/auth/src/common/login-strategies/sso-login.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
import { UnlockService } from "@bitwarden/unlock";

import { AuthRequestServiceAbstraction } from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
Expand Down Expand Up @@ -69,9 +70,9 @@ export class SsoLoginStrategy extends LoginStrategy {
constructor(
data: SsoLoginStrategyData,
private keyConnectorService: KeyConnectorService,
private unlockService: UnlockService,
private deviceTrustService: DeviceTrustServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private i18nService: I18nService,
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
) {
super(...sharedDeps);
Expand Down Expand Up @@ -136,7 +137,9 @@ export class SsoLoginStrategy extends LoginStrategy {
);
} else {
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
if (!(await this.configService.getFeatureFlag(FeatureFlag.UnlockKeyConnectorWithSdk))) {
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
}
}
}
}
Expand Down Expand Up @@ -187,6 +190,17 @@ export class SsoLoginStrategy extends LoginStrategy {

const userDecryptionOptions = tokenResponse?.userDecryptionOptions;

if (
tokenResponse.canUnlockWithKeyConnector() &&
(await this.configService.getFeatureFlag(FeatureFlag.UnlockKeyConnectorWithSdk))
) {
await this.unlockService.unlockWithKeyConnector(
userId,
tokenResponse.intoKeyConnectorUnlockData(),
);
return;
}

// Note: TDE and key connector are mutually exclusive
if (userDecryptionOptions?.trustedDeviceOption) {
this.logService.info("Attempting to set user key with approved admin auth request.");
Expand All @@ -205,7 +219,8 @@ export class SsoLoginStrategy extends LoginStrategy {
}
} else if (
masterKeyEncryptedUserKey != null &&
this.getKeyConnectorUrl(tokenResponse) != null
this.getKeyConnectorUrl(tokenResponse) != null &&
!(await this.configService.getFeatureFlag(FeatureFlag.UnlockKeyConnectorWithSdk))
) {
// Key connector enabled for user
await this.trySetUserKeyWithMasterKey(userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
Expand All @@ -30,6 +31,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { UnlockService } from "@bitwarden/unlock";

import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
Expand All @@ -54,6 +56,7 @@ describe("UserApiLoginStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let unlockService: MockProxy<UnlockService>;
let environmentService: MockProxy<EnvironmentService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
Expand Down Expand Up @@ -88,6 +91,7 @@ describe("UserApiLoginStrategy", () => {
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>();
unlockService = mock<UnlockService>();
environmentService = mock<EnvironmentService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
Expand All @@ -104,6 +108,7 @@ describe("UserApiLoginStrategy", () => {
apiLogInStrategy = new UserApiLoginStrategy(
cache,
keyConnectorService,
unlockService,
accountService,
masterPasswordService,
keyService,
Expand Down Expand Up @@ -209,6 +214,31 @@ describe("UserApiLoginStrategy", () => {
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
});

it("uses unlock service when SDK key connector feature flag is enabled", async () => {
const tokenResponse = identityTokenResponseFactory(undefined, {
HasMasterPassword: false,
KeyConnectorOption: { KeyConnectorUrl: keyConnectorUrl },
});
tokenResponse.apiUseKeyConnector = true;

const env = mock<Environment>();
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
environmentService.environment$ = new BehaviorSubject(env);
configService.getFeatureFlag
.calledWith(FeatureFlag.UnlockKeyConnectorWithSdk)
.mockResolvedValue(true);

apiService.postIdentityToken.mockResolvedValue(tokenResponse);

await apiLogInStrategy.logIn(credentials);

expect(unlockService.unlockWithKeyConnector).toHaveBeenCalledWith(userId, {
url: keyConnectorUrl,
keyConnectorKeyWrappedUserKey: tokenResponse.key!.encryptedString!,
});
expect(keyConnectorService.setMasterKeyFromUrl).not.toHaveBeenCalled();
});

it("decrypts and sets the user key if Key Connector is enabled", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { Jsonify } from "type-fest";

import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout";
import { UserId } from "@bitwarden/common/types/guid";
import { UnlockService } from "@bitwarden/unlock";

import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
Expand All @@ -30,6 +32,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
constructor(
data: UserApiLoginStrategyData,
private keyConnectorService: KeyConnectorService,
private unlockService: UnlockService,
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
) {
super(...sharedDeps);
Expand All @@ -52,7 +55,10 @@ export class UserApiLoginStrategy extends LoginStrategy {
}

protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
if (response.apiUseKeyConnector) {
if (
response.canUnlockWithKeyConnector() &&
!(await this.configService.getFeatureFlag(FeatureFlag.UnlockKeyConnectorWithSdk))
) {
const env = await firstValueFrom(this.environmentService.environment$);
const keyConnectorUrl = env.getKeyConnectorUrl();
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
Expand All @@ -63,11 +69,19 @@ export class UserApiLoginStrategy extends LoginStrategy {
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
if (response.key) {
if (
response.canUnlockWithKeyConnector() &&
(await this.configService.getFeatureFlag(FeatureFlag.UnlockKeyConnectorWithSdk))
) {
await this.unlockService.unlockWithKeyConnector(
userId,
response.intoKeyConnectorUnlockData(),
);
} else if (
response.canUnlockWithKeyConnector() &&
!(await this.configService.getFeatureFlag(FeatureFlag.UnlockKeyConnectorWithSdk))
) {
await this.masterPasswordService.setMasterKeyEncryptedUserKey(response.key, userId);
}

if (response.apiUseKeyConnector) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
Expand All @@ -76,6 +90,8 @@ export class UserApiLoginStrategy extends LoginStrategy {
);
await this.keyService.setUserKey(userKey, userId);
}
} else {
await this.masterPasswordService.setMasterKeyEncryptedUserKey(response.key, userId);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
KeyService,
PBKDF2KdfConfig,
} from "@bitwarden/key-management";
import { UnlockService } from "@bitwarden/unlock";

import {
AuthRequestServiceAbstraction,
Expand All @@ -70,6 +71,7 @@ describe("LoginStrategyService", () => {
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let unlockService: MockProxy<UnlockService>;
let environmentService: MockProxy<EnvironmentService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
Expand Down Expand Up @@ -103,6 +105,7 @@ describe("LoginStrategyService", () => {
messagingService = mock<MessagingService>();
logService = mock<LogService>();
keyConnectorService = mock<KeyConnectorService>();
unlockService = mock<UnlockService>();
environmentService = mock<EnvironmentService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
Expand Down Expand Up @@ -132,6 +135,7 @@ describe("LoginStrategyService", () => {
messagingService,
logService,
keyConnectorService,
unlockService,
environmentService,
stateService,
twoFactorService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
PBKDF2KdfConfig,
KdfConfigService,
} from "@bitwarden/key-management";
import { UnlockService } from "@bitwarden/unlock";

import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
Expand Down Expand Up @@ -145,6 +146,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected messagingService: MessagingService,
protected logService: LogService,
protected keyConnectorService: KeyConnectorService,
protected unlockService: UnlockService,
protected environmentService: EnvironmentService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
Expand Down Expand Up @@ -532,15 +534,16 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
return new SsoLoginStrategy(
data?.sso ?? new SsoLoginStrategyData(),
this.keyConnectorService,
this.unlockService,
this.deviceTrustService,
this.authRequestService,
this.i18nService,
...sharedDeps,
);
case AuthenticationType.UserApiKey:
return new UserApiLoginStrategy(
data?.userApiKey ?? new UserApiLoginStrategyData(),
this.keyConnectorService,
this.unlockService,
...sharedDeps,
);
case AuthenticationType.AuthRequest:
Expand Down
Loading
Loading