1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 11:43:46 +00:00
Files
browser/libs/auth/src/common/login-strategies/password-login.strategy.ts
Matt Gibson 9c1e2ebd67 Typescript-strict-plugin (#12235)
* Use typescript-strict-plugin to iteratively turn on strict

* Add strict testing to pipeline

Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files.

* turn on strict for scripts directory

* Use plugin for all tsconfigs in monorepo

vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes.

* remove plugin from configs that extend one that already has it

* Update workspace settings to honor strict plugin

* Apply strict-plugin to native message test runner

* Update vscode workspace to use root tsc version

* `./node_modules/.bin/update-strict-comments` 🤖

This is a one-time operation. All future files should adhere to strict type checking.

* Add fixme to `ts-strict-ignore` comments

* `update-strict-comments` 🤖

repeated for new merge files
2024-12-09 20:58:50 +01:00

237 lines
9.1 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs";
import { Jsonify } from "type-fest";
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";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
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 { UserId } from "@bitwarden/common/types/guid";
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, LoginStrategyData } from "./login.strategy";
export class PasswordLoginStrategyData implements LoginStrategyData {
tokenRequest: PasswordTokenRequest;
/** User's entered email obtained pre-login. Always present in MP login. */
userEnteredEmail: string;
/** If 2fa is required, token is returned to bypass captcha */
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 {
/** The email address of the user attempting to log in. */
email$: Observable<string>;
/** The master key hash used for authentication */
serverMasterKeyHash$: Observable<string>;
/** The local master key hash we store client side */
localMasterKeyHash$: Observable<string | null>;
protected cache: BehaviorSubject<PasswordLoginStrategyData>;
constructor(
data: PasswordLoginStrategyData,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService,
private loginStrategyService: LoginStrategyServiceAbstraction,
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
) {
super(...sharedDeps);
this.cache = new BehaviorSubject(data);
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
this.serverMasterKeyHash$ = this.cache.pipe(
map((state) => state.tokenRequest.masterPasswordHash),
);
this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
}
override async logIn(credentials: PasswordLoginCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials;
const data = new PasswordLoginStrategyData();
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
data.userEnteredEmail = email;
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
data.localMasterKeyHash = await this.keyService.hashMasterKey(
masterPassword,
data.masterKey,
HashPurpose.LocalAuthorization,
);
const serverMasterKeyHash = await this.keyService.hashMasterKey(masterPassword, data.masterKey);
data.tokenRequest = new PasswordTokenRequest(
email,
serverMasterKeyHash,
captchaToken,
await this.buildTwoFactor(twoFactor, email),
await this.buildDeviceRequest(),
);
this.cache.next(data);
const [authResult, identityResponse] = await this.startLogIn();
if (identityResponse instanceof IdentityCaptchaResponse) {
return authResult;
}
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
// The identity result can contain master password policies for the user's organizations
if (masterPasswordPolicyOptions?.enforceOnLogin) {
// If there is a policy active, evaluate the supplied password before its no longer in memory
const meetsRequirements = this.evaluateMasterPassword(
credentials,
masterPasswordPolicyOptions,
);
if (meetsRequirements) {
return authResult;
}
if (identityResponse instanceof IdentityTwoFactorResponse) {
// Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope
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.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword,
authResult.userId, // userId is only available on successful login
);
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
}
}
return authResult;
}
override async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string,
): Promise<AuthResult> {
const data = this.cache.value;
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
this.cache.next(data);
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.masterPasswordService.setForceSetPasswordReason(
forcePasswordResetReason,
result.userId,
);
result.forcePasswordReset = forcePasswordResetReason;
}
return result;
}
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
const { masterKey, localMasterKeyHash } = this.cache.value;
await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
}
protected override async setUserKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
// If migration is required, we won't have a user key to set yet.
if (this.encryptionKeyMigrationRequired(response)) {
return;
}
await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) {
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey,
userId,
);
await this.keyService.setUserKey(userKey, userId);
}
}
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.keyService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}
protected override encryptionKeyMigrationRequired(response: IdentityTokenResponse): boolean {
return !response.key;
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse,
): MasterPasswordPolicyOptions {
if (response == null) {
return null;
}
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
}
private evaluateMasterPassword(
{ masterPassword, email }: PasswordLoginCredentials,
options: MasterPasswordPolicyOptions,
): boolean {
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
email,
)?.score;
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
}
exportCache(): CacheData {
return {
password: this.cache.value,
};
}
}