mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 01:33:33 +00:00
[PM-5255, PM-3339] Refactor login strategy to use state providers (#7821)
* add key definition and StrategyData classes * use state providers for login strategies * serialize login data for cache * use state providers for auth request notification * fix registrations * add docs to abstraction * fix sso strategy * fix password login strategy tests * fix base login strategy tests * fix user api login strategy tests * PM-3339 add tests for admin auth request in sso strategy * fix auth request login strategy tests * fix webauthn login strategy tests * create login strategy state * use barrel file in common/spec * test login strategy cache deserialization * use global state provider * add test for login strategy service * fix auth request storage * add recursive prototype checking and json deserializers to nested objects * fix CLI * Create wrapper for login strategy cache * use behavior subjects in strategies instead of global state * rename userApi to userApiKey * pr feedback * fix tests * fix deserialization tests * fix tests --------- Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { BehaviorSubject, map, Observable } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
@@ -17,35 +20,56 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy } from "./login.strategy";
|
||||
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
|
||||
export class PasswordLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
captchaBypassToken?: string;
|
||||
/**
|
||||
* The local version of the user's master key hash
|
||||
*/
|
||||
localMasterKeyHash: string;
|
||||
/**
|
||||
* The user's master key
|
||||
*/
|
||||
masterKey: MasterKey;
|
||||
/**
|
||||
* Tracks if the user needs to update their password due to
|
||||
* a password that does not meet an organization's master password policy.
|
||||
*/
|
||||
forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
|
||||
static fromJSON(obj: Jsonify<PasswordLoginStrategyData>): PasswordLoginStrategyData {
|
||||
const data = Object.assign(new PasswordLoginStrategyData(), obj, {
|
||||
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
|
||||
masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordLoginStrategy extends LoginStrategy {
|
||||
get email() {
|
||||
return this.tokenRequest.email;
|
||||
}
|
||||
|
||||
get masterPasswordHash() {
|
||||
return this.tokenRequest.masterPasswordHash;
|
||||
}
|
||||
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
|
||||
private localMasterKeyHash: string;
|
||||
private masterKey: MasterKey;
|
||||
|
||||
/**
|
||||
* Options to track if the user needs to update their password due to a password that does not meet an organization's
|
||||
* master password policy.
|
||||
* The email address of the user attempting to log in.
|
||||
*/
|
||||
private forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
email$: Observable<string>;
|
||||
/**
|
||||
* The master key hash of the user attempting to log in.
|
||||
*/
|
||||
masterKeyHash$: Observable<string | null>;
|
||||
|
||||
protected cache: BehaviorSubject<PasswordLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: PasswordLoginStrategyData,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
@@ -70,42 +94,27 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
);
|
||||
}
|
||||
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
const result = await super.logInTwoFactor(twoFactor);
|
||||
|
||||
// 2FA was successful, save the force update password options with the state service if defined
|
||||
if (
|
||||
!result.requiresTwoFactor &&
|
||||
!result.requiresCaptcha &&
|
||||
this.forcePasswordResetReason != ForceSetPasswordReason.None
|
||||
) {
|
||||
await this.stateService.setForceSetPasswordReason(this.forcePasswordResetReason);
|
||||
result.forcePasswordReset = this.forcePasswordResetReason;
|
||||
}
|
||||
|
||||
return result;
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
|
||||
this.masterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
|
||||
}
|
||||
|
||||
override async logIn(credentials: PasswordLoginCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
|
||||
this.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
|
||||
const data = new PasswordLoginStrategyData();
|
||||
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
|
||||
|
||||
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
||||
this.localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
data.localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
masterPassword,
|
||||
this.masterKey,
|
||||
data.masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, this.masterKey);
|
||||
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, data.masterKey);
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
data.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
masterKeyHash,
|
||||
captchaToken,
|
||||
@@ -113,6 +122,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
|
||||
this.cache.next(data);
|
||||
|
||||
const [authResult, identityResponse] = await this.startLogIn();
|
||||
|
||||
const masterPasswordPolicyOptions =
|
||||
@@ -129,7 +140,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
if (!meetsRequirements) {
|
||||
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
|
||||
// Save the flag to this strategy for later use as the master password is about to pass out of scope
|
||||
this.forcePasswordResetReason = ForceSetPasswordReason.WeakMasterPassword;
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||
});
|
||||
} else {
|
||||
// Authentication was successful, save the force update password options with the state service
|
||||
await this.stateService.setForceSetPasswordReason(
|
||||
@@ -142,9 +156,34 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
captchaBypassToken: captchaResponse ?? this.cache.value.captchaBypassToken,
|
||||
});
|
||||
const result = await super.logInTwoFactor(twoFactor);
|
||||
|
||||
// 2FA was successful, save the force update password options with the state service if defined
|
||||
const forcePasswordResetReason = this.cache.value.forcePasswordResetReason;
|
||||
if (
|
||||
!result.requiresTwoFactor &&
|
||||
!result.requiresCaptcha &&
|
||||
forcePasswordResetReason != ForceSetPasswordReason.None
|
||||
) {
|
||||
await this.stateService.setForceSetPasswordReason(forcePasswordResetReason);
|
||||
result.forcePasswordReset = forcePasswordResetReason;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
await this.cryptoService.setMasterKey(this.masterKey);
|
||||
await this.cryptoService.setMasterKeyHash(this.localMasterKeyHash);
|
||||
const { masterKey, localMasterKeyHash } = this.cache.value;
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
await this.cryptoService.setMasterKeyHash(localMasterKeyHash);
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
@@ -191,4 +230,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
|
||||
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
password: this.cache.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user