[PM-6727] Part 1: pass userId in login strategies (#9030)
* add validation to initAccount * pass userId to setMasterKey * fix key connector tests
This commit is contained in:
parent
debfe914c2
commit
69ed6ce1f5
|
@ -117,13 +117,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||||
return super.logInTwoFactor(twoFactor);
|
return super.logInTwoFactor(twoFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||||
if (
|
if (
|
||||||
authRequestCredentials.decryptedMasterKey &&
|
authRequestCredentials.decryptedMasterKey &&
|
||||||
authRequestCredentials.decryptedMasterKeyHash
|
authRequestCredentials.decryptedMasterKeyHash
|
||||||
) {
|
) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(
|
await this.masterPasswordService.setMasterKey(
|
||||||
authRequestCredentials.decryptedMasterKey,
|
authRequestCredentials.decryptedMasterKey,
|
||||||
userId,
|
userId,
|
||||||
|
@ -147,15 +146,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||||
if (authRequestCredentials.decryptedUserKey) {
|
if (authRequestCredentials.decryptedUserKey) {
|
||||||
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
||||||
} else {
|
} else {
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey(userId);
|
||||||
|
|
||||||
// Establish trust if required after setting user key
|
// Establish trust if required after setting user key
|
||||||
await this.deviceTrustService.trustDeviceIfRequired(userId);
|
await this.deviceTrustService.trustDeviceIfRequired(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
|
|
@ -244,6 +244,7 @@ describe("LoginStrategy", () => {
|
||||||
const result = await passwordLoginStrategy.logIn(credentials);
|
const result = await passwordLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
userId: userId,
|
||||||
forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
|
forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
|
||||||
resetMasterPassword: true,
|
resetMasterPassword: true,
|
||||||
twoFactorProviders: null,
|
twoFactorProviders: null,
|
||||||
|
|
|
@ -241,6 +241,7 @@ export abstract class LoginStrategy {
|
||||||
|
|
||||||
// Must come before setting keys, user key needs email to update additional keys
|
// Must come before setting keys, user key needs email to update additional keys
|
||||||
const userId = await this.saveAccountInformation(response);
|
const userId = await this.saveAccountInformation(response);
|
||||||
|
result.userId = userId;
|
||||||
|
|
||||||
if (response.twoFactorToken != null) {
|
if (response.twoFactorToken != null) {
|
||||||
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
||||||
|
@ -249,7 +250,7 @@ export abstract class LoginStrategy {
|
||||||
await this.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken);
|
await this.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setMasterKey(response);
|
await this.setMasterKey(response, userId);
|
||||||
await this.setUserKey(response, userId);
|
await this.setUserKey(response, userId);
|
||||||
await this.setPrivateKey(response);
|
await this.setPrivateKey(response);
|
||||||
|
|
||||||
|
@ -259,7 +260,7 @@ export abstract class LoginStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The keys comes from different sources depending on the login strategy
|
// The keys comes from different sources depending on the login strategy
|
||||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||||
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
|
|
||||||
|
|
|
@ -173,8 +173,11 @@ describe("PasswordLoginStrategy", () => {
|
||||||
localHashedPassword,
|
localHashedPassword,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
tokenResponse.key,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
const [authResult, identityResponse] = await this.startLogIn();
|
const [authResult, identityResponse] = await this.startLogIn();
|
||||||
|
|
||||||
|
if (identityResponse instanceof IdentityCaptchaResponse) {
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
const masterPasswordPolicyOptions =
|
const masterPasswordPolicyOptions =
|
||||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||||
|
|
||||||
|
@ -157,23 +161,23 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
credentials,
|
credentials,
|
||||||
masterPasswordPolicyOptions,
|
masterPasswordPolicyOptions,
|
||||||
);
|
);
|
||||||
|
if (meetsRequirements) {
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!meetsRequirements) {
|
if (identityResponse instanceof IdentityTwoFactorResponse) {
|
||||||
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
|
// Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope
|
||||||
// Save the flag to this strategy for later use as the master password is about to pass out of scope
|
this.cache.next({
|
||||||
this.cache.next({
|
...this.cache.value,
|
||||||
...this.cache.value,
|
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
// Authentication was successful, save the force update password options with the state service
|
||||||
// Authentication was successful, save the force update password options with the state service
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
ForceSetPasswordReason.WeakMasterPassword,
|
||||||
await this.masterPasswordService.setForceSetPasswordReason(
|
authResult.userId, // userId is only available on successful login
|
||||||
ForceSetPasswordReason.WeakMasterPassword,
|
);
|
||||||
userId,
|
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
||||||
);
|
|
||||||
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return authResult;
|
return authResult;
|
||||||
|
@ -196,17 +200,18 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
!result.requiresCaptcha &&
|
!result.requiresCaptcha &&
|
||||||
forcePasswordResetReason != ForceSetPasswordReason.None
|
forcePasswordResetReason != ForceSetPasswordReason.None
|
||||||
) {
|
) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId);
|
forcePasswordResetReason,
|
||||||
|
result.userId,
|
||||||
|
);
|
||||||
result.forcePasswordReset = forcePasswordResetReason;
|
result.forcePasswordReset = forcePasswordResetReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
const { masterKey, localMasterKeyHash } = this.cache.value;
|
const { masterKey, localMasterKeyHash } = this.cache.value;
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||||
}
|
}
|
||||||
|
@ -219,12 +224,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
if (this.encryptionKeyMigrationRequired(response)) {
|
if (this.encryptionKeyMigrationRequired(response)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
|
||||||
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey);
|
await this.cryptoService.setUserKey(userKey, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,9 +244,9 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMasterPasswordPolicyOptionsFromResponse(
|
private getMasterPasswordPolicyOptionsFromResponse(
|
||||||
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse,
|
response: IdentityTokenResponse | IdentityTwoFactorResponse,
|
||||||
): MasterPasswordPolicyOptions {
|
): MasterPasswordPolicyOptions {
|
||||||
if (response == null || response instanceof IdentityCaptchaResponse) {
|
if (response == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
||||||
|
|
|
@ -163,7 +163,10 @@ describe("SsoLoginStrategy", () => {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
|
tokenResponse.key,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Trusted Device Decryption", () => {
|
describe("Trusted Device Decryption", () => {
|
||||||
|
@ -417,7 +420,7 @@ describe("SsoLoginStrategy", () => {
|
||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
||||||
|
@ -430,6 +433,7 @@ describe("SsoLoginStrategy", () => {
|
||||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
ssoOrgId,
|
ssoOrgId,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -468,7 +472,7 @@ describe("SsoLoginStrategy", () => {
|
||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
||||||
|
@ -481,6 +485,7 @@ describe("SsoLoginStrategy", () => {
|
||||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
ssoOrgId,
|
ssoOrgId,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
|
@ -124,7 +125,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
async logIn(credentials: SsoLoginCredentials) {
|
async logIn(credentials: SsoLoginCredentials): Promise<AuthResult> {
|
||||||
const data = new SsoLoginStrategyData();
|
const data = new SsoLoginStrategyData();
|
||||||
data.orgId = credentials.orgId;
|
data.orgId = credentials.orgId;
|
||||||
|
|
||||||
|
@ -147,10 +148,9 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
// Auth guard currently handles redirects for this.
|
// Auth guard currently handles redirects for this.
|
||||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setForceSetPasswordReason(
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
ssoAuthResult.forcePasswordReset,
|
ssoAuthResult.forcePasswordReset,
|
||||||
userId,
|
ssoAuthResult.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
return ssoAuthResult;
|
return ssoAuthResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
|
protected override async setMasterKey(tokenResponse: IdentityTokenResponse, userId: UserId) {
|
||||||
// The only way we can be setting a master key at this point is if we are using Key Connector.
|
// The only way we can be setting a master key at this point is if we are using Key Connector.
|
||||||
// First, check to make sure that we should do so based on the token response.
|
// First, check to make sure that we should do so based on the token response.
|
||||||
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
|
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
|
||||||
|
@ -175,10 +175,11 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||||
tokenResponse,
|
tokenResponse,
|
||||||
this.cache.value.orgId,
|
this.cache.value.orgId,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
||||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,7 +232,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
if (masterKeyEncryptedUserKey) {
|
if (masterKeyEncryptedUserKey) {
|
||||||
// set the master key encrypted user key if it exists
|
// set the master key encrypted user key if it exists
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||||
|
@ -251,7 +252,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
this.getKeyConnectorUrl(tokenResponse) != null
|
this.getKeyConnectorUrl(tokenResponse) != null
|
||||||
) {
|
) {
|
||||||
// Key connector enabled for user
|
// Key connector enabled for user
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: In the traditional SSO flow with MP without key connector, the lock component
|
// Note: In the traditional SSO flow with MP without key connector, the lock component
|
||||||
|
@ -338,8 +339,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
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
|
// There is a scenario in which the master key is not set here. That will occur if the user
|
||||||
|
|
|
@ -174,7 +174,7 @@ describe("UserApiLoginStrategy", () => {
|
||||||
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
await apiLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
||||||
|
@ -195,6 +195,6 @@ describe("UserApiLoginStrategy", () => {
|
||||||
await apiLogInStrategy.logIn(credentials);
|
await apiLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -93,11 +93,11 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
const env = await firstValueFrom(this.environmentService.environment$);
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
const keyConnectorUrl = env.getKeyConnectorUrl();
|
const keyConnectorUrl = env.getKeyConnectorUrl();
|
||||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,11 +108,10 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||||
|
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey);
|
await this.cryptoService.setUserKey(userKey, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,6 +122,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overridden to save client ID and secret to token service
|
||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
||||||
const userId = await super.saveAccountInformation(tokenResponse);
|
const userId = await super.saveAccountInformation(tokenResponse);
|
||||||
|
|
||||||
|
|
|
@ -208,7 +208,10 @@ describe("WebAuthnLoginStrategy", () => {
|
||||||
// Assert
|
// Assert
|
||||||
// Master key encrypted user key should be set
|
// Master key encrypted user key should be set
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(idTokenResponse.key);
|
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
|
idTokenResponse.key,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
expect(cryptoService.decryptToBytes).toHaveBeenCalledTimes(1);
|
expect(cryptoService.decryptToBytes).toHaveBeenCalledTimes(1);
|
||||||
expect(cryptoService.decryptToBytes).toHaveBeenCalledWith(
|
expect(cryptoService.decryptToBytes).toHaveBeenCalledWith(
|
||||||
|
@ -220,7 +223,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||||
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
|
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
|
||||||
mockPrfPrivateKey,
|
mockPrfPrivateKey,
|
||||||
);
|
);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
|
||||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
|
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
|
||||||
|
|
||||||
// Master key and private key should not be set
|
// Master key and private key should not be set
|
||||||
|
|
|
@ -98,7 +98,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||||
throw new Error("2FA not supported yet for WebAuthn Login.");
|
throw new Error("2FA not supported yet for WebAuthn Login.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey() {
|
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
if (masterKeyEncryptedUserKey) {
|
if (masterKeyEncryptedUserKey) {
|
||||||
// set the master key encrypted user key if it exists
|
// set the master key encrypted user key if it exists
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userDecryptionOptions = idTokenResponse?.userDecryptionOptions;
|
const userDecryptionOptions = idTokenResponse?.userDecryptionOptions;
|
||||||
|
@ -134,7 +134,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey);
|
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { Organization } from "../../admin-console/models/domain/organization";
|
import { Organization } from "../../admin-console/models/domain/organization";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||||
|
|
||||||
export abstract class KeyConnectorService {
|
export abstract class KeyConnectorService {
|
||||||
setMasterKeyFromUrl: (url?: string) => Promise<void>;
|
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
|
||||||
getManagingOrganization: () => Promise<Organization>;
|
getManagingOrganization: () => Promise<Organization>;
|
||||||
getUsesKeyConnector: () => Promise<boolean>;
|
getUsesKeyConnector: () => Promise<boolean>;
|
||||||
migrateUser: () => Promise<void>;
|
migrateUser: () => Promise<void>;
|
||||||
|
@ -10,6 +11,7 @@ export abstract class KeyConnectorService {
|
||||||
convertNewSsoUserToKeyConnector: (
|
convertNewSsoUserToKeyConnector: (
|
||||||
tokenResponse: IdentityTokenResponse,
|
tokenResponse: IdentityTokenResponse,
|
||||||
orgId: string,
|
orgId: string,
|
||||||
|
userId: UserId,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
||||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
import { Utils } from "../../../platform/misc/utils";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
||||||
|
|
||||||
import { ForceSetPasswordReason } from "./force-set-password-reason";
|
import { ForceSetPasswordReason } from "./force-set-password-reason";
|
||||||
|
|
||||||
export class AuthResult {
|
export class AuthResult {
|
||||||
|
userId: UserId;
|
||||||
captchaSiteKey = "";
|
captchaSiteKey = "";
|
||||||
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -215,7 +215,7 @@ describe("KeyConnectorService", () => {
|
||||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await keyConnectorService.setMasterKeyFromUrl(url);
|
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||||
|
@ -235,7 +235,7 @@ describe("KeyConnectorService", () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Act
|
// Act
|
||||||
await keyConnectorService.setMasterKeyFromUrl(url);
|
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||||
} catch {
|
} catch {
|
||||||
// Assert
|
// Assert
|
||||||
expect(logService.error).toHaveBeenCalledWith(error);
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
StateProvider,
|
StateProvider,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { MasterKey } from "../../types/key";
|
import { MasterKey } from "../../types/key";
|
||||||
import { AccountService } from "../abstractions/account.service";
|
import { AccountService } from "../abstractions/account.service";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||||
|
@ -100,12 +101,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
||||||
async setMasterKeyFromUrl(url: string) {
|
async setMasterKeyFromUrl(url: string, userId: UserId) {
|
||||||
try {
|
try {
|
||||||
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
||||||
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
||||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.handleKeyConnectorError(e);
|
this.handleKeyConnectorError(e);
|
||||||
|
@ -123,7 +123,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
async convertNewSsoUserToKeyConnector(
|
||||||
|
tokenResponse: IdentityTokenResponse,
|
||||||
|
orgId: string,
|
||||||
|
userId: UserId,
|
||||||
|
) {
|
||||||
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||||
const {
|
const {
|
||||||
kdf,
|
kdf,
|
||||||
|
@ -145,12 +149,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||||
kdfConfig,
|
kdfConfig,
|
||||||
);
|
);
|
||||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
|
|
||||||
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey[0]);
|
await this.cryptoService.setUserKey(userKey[0], userId);
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
|
||||||
|
|
||||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
||||||
|
|
||||||
|
|
|
@ -771,6 +771,19 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: EncString;
|
privateKey: EncString;
|
||||||
}> {
|
}> {
|
||||||
|
// Verify keys don't exist
|
||||||
|
const existingUserKey = await this.getUserKey();
|
||||||
|
const existingPrivateKey = await this.getPrivateKey();
|
||||||
|
if (existingUserKey != null || existingPrivateKey != null) {
|
||||||
|
if (existingUserKey != null) {
|
||||||
|
this.logService.error("Tried to initialize account with existing user key.");
|
||||||
|
}
|
||||||
|
if (existingPrivateKey != null) {
|
||||||
|
this.logService.error("Tried to initialize account with existing private key.");
|
||||||
|
}
|
||||||
|
throw new Error("Cannot initialize account, keys already exist.");
|
||||||
|
}
|
||||||
|
|
||||||
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
|
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
|
||||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||||
await this.setUserKey(userKey);
|
await this.setUserKey(userKey);
|
||||||
|
|
Loading…
Reference in New Issue