mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
PM-19061 - Innovation Sprint - add OPAQUE Login Strategy (#13832)
* ChangePassword - add TODOs to clean up code * LoginComp - Add TODOs for identifying the login strategy ahead of time. * DefaultOpaqueService - Add TODOs * PasswordLoginStrategy - add TODO for renaming * WIP first draft of opaque login strategy * Per discussion with platform, we don't need an abstraction for api services so clean that up. * Extract pre-login method into own service from ApiService + move request model to auth * LoginStrategyService - add todo for adding support for opaque login strategy * PreLoginApiService - add renaming todo * LoginComp + PasswordLoginCredentials - (1) Start integrating pre-login logic into login comp (2) update PasswordLoginCredentials to include kdfConfig to pass into login strat * LoginStrategyServiceAbstraction - login - add OpaqueLoginCredentials * CLI - add todos * LoginComp - add TODO * Add createKdfConfig factory function * LoginStrategyService: switch out to more specific password strategy * Fix type errors * Add jsdoc * Revert / remove TODOs and old draft work * add missing dep * PreLoginResponse - Adjust KM import * PreLogin renamed to PrePasswordLogin * Renames + some login strategy service test updates * LoginComp - remove unused import * KdfConfig - Rename validateKdfConfigForPrelogin to validateKdfConfigForPreLogin * LoginStrategyService - (1) Rename makePreloginKey to makePrePasswordLoginMasterKey (2) Refactor makePrePasswordLoginMasterKey to accept an optional KdfConfig so we can keep the logic tested on the LoginStrategyService * LoginStrategyService - add TODOs * Fix non-sdk build errors --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -225,6 +225,8 @@ export class LoginCommand {
|
||||
"Encryption key migration required. Please login through the web vault to update your encryption key.",
|
||||
);
|
||||
}
|
||||
// TODO: PM-15162 - captcha is deprecated as part of UI refresh work
|
||||
|
||||
if (response.captchaSiteKey) {
|
||||
const credentials = new PasswordLoginCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
||||
@@ -88,8 +88,16 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
const request = new TwoFactorRecoveryRequest();
|
||||
request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase();
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email);
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(this.masterPassword, key);
|
||||
|
||||
const masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey(
|
||||
this.masterPassword,
|
||||
request.email,
|
||||
);
|
||||
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
masterKey,
|
||||
);
|
||||
|
||||
if (this.recoveryCodeLoginFeatureFlagEnabled) {
|
||||
await this.handleRecoveryLogin(request);
|
||||
|
||||
@@ -231,11 +231,10 @@ export class ChangePasswordComponent
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
await this.opaqueService.login(this.email, this.masterPassword, {
|
||||
memory: 256 * 1024,
|
||||
iterations: 3,
|
||||
parallelism: 4,
|
||||
// TODO: remove this test code
|
||||
await this.opaqueService.register(this.masterPassword, newUserKey[0], {
|
||||
algorithm: "argon2id",
|
||||
parameters: { memory: 256 * 1024, iterations: 3, parallelism: 4 },
|
||||
});
|
||||
|
||||
this.toastService.showToast({
|
||||
|
||||
@@ -104,7 +104,6 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { DefaultOpaqueApiService } from "@bitwarden/common/auth/opaque/default-opaque-api.service";
|
||||
import { DefaultOpaqueService } from "@bitwarden/common/auth/opaque/default-opaque.service";
|
||||
import { OpaqueApiService } from "@bitwarden/common/auth/opaque/opaque-api.service";
|
||||
import { OpaqueService } from "@bitwarden/common/auth/opaque/opaque.service";
|
||||
@@ -119,6 +118,7 @@ import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
|
||||
import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
@@ -472,6 +472,7 @@ const safeProviders: SafeProvider[] = [
|
||||
VaultTimeoutSettingsService,
|
||||
KdfConfigService,
|
||||
TaskSchedulerService,
|
||||
PrePasswordLoginApiService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1479,9 +1480,12 @@ const safeProviders: SafeProvider[] = [
|
||||
ToastService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PrePasswordLoginApiService,
|
||||
deps: [ApiServiceAbstraction, EnvironmentService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OpaqueApiService,
|
||||
useClass: DefaultOpaqueApiService,
|
||||
deps: [ApiServiceAbstraction, EnvironmentService],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
UserApiLoginCredentials,
|
||||
@@ -71,8 +72,15 @@ export abstract class LoginStrategyServiceAbstraction {
|
||||
) => Promise<AuthResult>;
|
||||
/**
|
||||
* Creates a master key from the provided master password and email.
|
||||
* If a KdfConfig is provided, it will be used to generate the key.
|
||||
* Otherwise, the PrePasswordLogin endpoint will be used to retrieve the user's
|
||||
* KdfConfig.
|
||||
*/
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||
makePrePasswordLoginMasterKey: (
|
||||
masterPassword: string,
|
||||
email: string,
|
||||
kdfConfig?: KdfConfig,
|
||||
) => Promise<MasterKey>;
|
||||
/**
|
||||
* Emits true if the authentication session has expired.
|
||||
*/
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
AuthRequestLoginStrategy,
|
||||
AuthRequestLoginStrategyData,
|
||||
} from "./auth-request-login.strategy";
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { identityTokenResponseFactory } from "./base-login.strategy.spec";
|
||||
|
||||
describe("AuthRequestLoginStrategy", () => {
|
||||
let cache: AuthRequestLoginStrategyData;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy";
|
||||
|
||||
export class AuthRequestLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
@@ -29,7 +29,7 @@ export class AuthRequestLoginStrategyData implements LoginStrategyData {
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
export class AuthRequestLoginStrategy extends BaseLoginStrategy {
|
||||
email$: Observable<string>;
|
||||
accessCode$: Observable<string>;
|
||||
authRequestId$: Observable<string>;
|
||||
@@ -39,7 +39,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
constructor(
|
||||
data: AuthRequestLoginStrategyData,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request";
|
||||
import { OpaqueTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/opaque-token.request";
|
||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
@@ -45,10 +46,11 @@ import {
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import {
|
||||
UserApiLoginCredentials,
|
||||
PasswordLoginCredentials,
|
||||
SsoLoginCredentials,
|
||||
AuthRequestLoginCredentials,
|
||||
WebAuthnLoginCredentials,
|
||||
OpaqueLoginCredentials,
|
||||
PasswordHashLoginCredentials,
|
||||
} from "../models/domain/login-credentials";
|
||||
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
@@ -63,6 +65,7 @@ export abstract class LoginStrategyData {
|
||||
tokenRequest:
|
||||
| UserApiTokenRequest
|
||||
| PasswordTokenRequest
|
||||
| OpaqueTokenRequest
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest
|
||||
| undefined;
|
||||
@@ -72,7 +75,7 @@ export abstract class LoginStrategyData {
|
||||
abstract userEnteredEmail?: string;
|
||||
}
|
||||
|
||||
export abstract class LoginStrategy {
|
||||
export abstract class BaseLoginStrategy {
|
||||
protected abstract cache: BehaviorSubject<LoginStrategyData>;
|
||||
protected sessionTimeoutSubject = new BehaviorSubject<boolean>(false);
|
||||
sessionTimeout$: Observable<boolean> = this.sessionTimeoutSubject.asObservable();
|
||||
@@ -102,10 +105,11 @@ export abstract class LoginStrategy {
|
||||
abstract logIn(
|
||||
credentials:
|
||||
| UserApiLoginCredentials
|
||||
| PasswordLoginCredentials
|
||||
| PasswordHashLoginCredentials
|
||||
| SsoLoginCredentials
|
||||
| AuthRequestLoginCredentials
|
||||
| WebAuthnLoginCredentials,
|
||||
| WebAuthnLoginCredentials
|
||||
| OpaqueLoginCredentials,
|
||||
): Promise<AuthResult>;
|
||||
|
||||
async logInTwoFactor(
|
||||
@@ -0,0 +1 @@
|
||||
// TODO: add tests for OpaqueLoginStrategy once it is implemented
|
||||
262
libs/auth/src/common/login-strategies/opaque-login.strategy.ts
Normal file
262
libs/auth/src/common/login-strategies/opaque-login.strategy.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
// 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 { OpaqueTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/opaque-token.request";
|
||||
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 { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.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 { OpaqueLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy";
|
||||
|
||||
export class OpaqueLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: OpaqueTokenRequest;
|
||||
|
||||
/** User's entered email obtained pre-login. Always present in MP login. */
|
||||
userEnteredEmail: 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 be forced to update their password
|
||||
*/
|
||||
forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
|
||||
static fromJSON(obj: Jsonify<OpaqueLoginStrategyData>): OpaqueLoginStrategyData {
|
||||
const data = Object.assign(new OpaqueLoginStrategyData(), obj, {
|
||||
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
|
||||
masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: link to RFC and give simple, brief explanation of the protocol
|
||||
/**
|
||||
*
|
||||
* A login strategy that uses the ...
|
||||
*/
|
||||
export class OpaqueLoginStrategy extends BaseLoginStrategy {
|
||||
/** The email address of the user attempting to log in. */
|
||||
email$: Observable<string>;
|
||||
|
||||
/** The local master key hash we store client side */
|
||||
localMasterKeyHash$: Observable<string | null>;
|
||||
|
||||
protected cache: BehaviorSubject<OpaqueLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: OpaqueLoginStrategyData,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
|
||||
|
||||
this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
|
||||
}
|
||||
|
||||
// TODO: build OpaqueLoginCredentials
|
||||
override async logIn(credentials: OpaqueLoginCredentials) {
|
||||
const { email, masterPassword, twoFactor } = credentials;
|
||||
|
||||
const data = new OpaqueLoginStrategyData();
|
||||
|
||||
// TODO: we will still generate a master key here but we need to extract the prelogin call out of the makePreloginKey
|
||||
// and simply rename it deriveMasterKey or something similar
|
||||
data.masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey(
|
||||
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);
|
||||
|
||||
// TODO: we must figure out how we will handle 2FA at some point.
|
||||
data.tokenRequest = new OpaqueTokenRequest(
|
||||
email,
|
||||
undefined,
|
||||
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): Promise<AuthResult> {
|
||||
const data = this.cache.value;
|
||||
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;
|
||||
}
|
||||
|
||||
// We still need this for local user verification scenarios
|
||||
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
|
||||
| IdentityDeviceVerificationResponse,
|
||||
): MasterPasswordPolicyOptions | null {
|
||||
if (
|
||||
response == null ||
|
||||
response instanceof IdentityDeviceVerificationResponse ||
|
||||
response.masterPasswordPolicy == null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
||||
}
|
||||
|
||||
private evaluateMasterPassword(
|
||||
{ masterPassword, email }: OpaqueLoginCredentials,
|
||||
options: MasterPasswordPolicyOptions,
|
||||
): boolean {
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
email,
|
||||
)?.score;
|
||||
|
||||
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
opaque: this.cache.value,
|
||||
};
|
||||
}
|
||||
|
||||
async logInNewDeviceVerification(deviceVerificationOtp: string): Promise<AuthResult> {
|
||||
const data = this.cache.value;
|
||||
data.tokenRequest.newDeviceOtp = deviceVerificationOtp;
|
||||
this.cache.next(data);
|
||||
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { identityTokenResponseFactory } from "./base-login.strategy.spec";
|
||||
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
|
||||
|
||||
const email = "hello@world.com";
|
||||
@@ -115,7 +115,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
sub: userId,
|
||||
});
|
||||
|
||||
loginStrategyService.makePreloginKey.mockResolvedValue(masterKey);
|
||||
loginStrategyService.makePrePasswordLoginMasterKey.mockResolvedValue(masterKey);
|
||||
|
||||
keyService.hashMasterKey
|
||||
.calledWith(masterPassword, expect.anything(), undefined)
|
||||
|
||||
@@ -20,11 +20,12 @@ 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 { PasswordHashLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy";
|
||||
|
||||
// TODO: consider renaming LegacyPasswordLoginStrategy? Or PasswordHashLoginStrategy?
|
||||
export class PasswordLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
|
||||
@@ -51,7 +52,7 @@ export class PasswordLoginStrategyData implements LoginStrategyData {
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordLoginStrategy extends LoginStrategy {
|
||||
export class PasswordLoginStrategy extends BaseLoginStrategy {
|
||||
/** The email address of the user attempting to log in. */
|
||||
email$: Observable<string>;
|
||||
/** The master key hash used for authentication */
|
||||
@@ -66,7 +67,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
@@ -78,11 +79,16 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
|
||||
}
|
||||
|
||||
override async logIn(credentials: PasswordLoginCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
override async logIn(credentials: PasswordHashLoginCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor, kdfConfig } = credentials;
|
||||
|
||||
const data = new PasswordLoginStrategyData();
|
||||
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
|
||||
|
||||
data.masterKey = await this.loginStrategyService.makePrePasswordLoginMasterKey(
|
||||
masterPassword,
|
||||
email,
|
||||
kdfConfig,
|
||||
);
|
||||
data.userEnteredEmail = email;
|
||||
|
||||
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
||||
@@ -221,7 +227,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
}
|
||||
|
||||
private evaluateMasterPassword(
|
||||
{ masterPassword, email }: PasswordLoginCredentials,
|
||||
{ masterPassword, email }: PasswordHashLoginCredentials,
|
||||
options: MasterPasswordPolicyOptions,
|
||||
): boolean {
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
} from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { identityTokenResponseFactory } from "./base-login.strategy.spec";
|
||||
import { SsoLoginStrategy } from "./sso-login.strategy";
|
||||
|
||||
describe("SsoLoginStrategy", () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategyData, LoginStrategy } from "./login.strategy";
|
||||
import { LoginStrategyData, BaseLoginStrategy } from "./base-login.strategy";
|
||||
|
||||
export class SsoLoginStrategyData implements LoginStrategyData {
|
||||
captchaBypassToken: string;
|
||||
@@ -51,7 +51,7 @@ export class SsoLoginStrategyData implements LoginStrategyData {
|
||||
}
|
||||
}
|
||||
|
||||
export class SsoLoginStrategy extends LoginStrategy {
|
||||
export class SsoLoginStrategy extends BaseLoginStrategy {
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.email}
|
||||
*/
|
||||
@@ -73,7 +73,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { identityTokenResponseFactory } from "./base-login.strategy.spec";
|
||||
import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login.strategy";
|
||||
|
||||
describe("UserApiLoginStrategy", () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy";
|
||||
|
||||
export class UserApiLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: UserApiTokenRequest;
|
||||
@@ -25,13 +25,13 @@ export class UserApiLoginStrategyData implements LoginStrategyData {
|
||||
}
|
||||
}
|
||||
|
||||
export class UserApiLoginStrategy extends LoginStrategy {
|
||||
export class UserApiLoginStrategy extends BaseLoginStrategy {
|
||||
protected cache: BehaviorSubject<UserApiLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: UserApiLoginStrategyData,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { identityTokenResponseFactory } from "./base-login.strategy.spec";
|
||||
import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-login.strategy";
|
||||
|
||||
describe("WebAuthnLoginStrategy", () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
import { BaseLoginStrategy, LoginStrategyData } from "./base-login.strategy";
|
||||
|
||||
export class WebAuthnLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: WebAuthnLoginTokenRequest;
|
||||
@@ -29,12 +29,12 @@ export class WebAuthnLoginStrategyData implements LoginStrategyData {
|
||||
}
|
||||
}
|
||||
|
||||
export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
export class WebAuthnLoginStrategy extends BaseLoginStrategy {
|
||||
protected cache: BehaviorSubject<WebAuthnLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: WebAuthnLoginStrategyData,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
|
||||
) {
|
||||
super(...sharedDeps);
|
||||
|
||||
|
||||
@@ -4,10 +4,28 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { PrePasswordLoginResponse } from "@bitwarden/common/auth/models/response/pre-password-login.response";
|
||||
import { CipherConfiguration } from "@bitwarden/common/auth/opaque/models/cipher-configuration";
|
||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { KdfConfig } from "../../../../../key-management/src";
|
||||
|
||||
export type LoginCredentials =
|
||||
| PasswordLoginCredentials
|
||||
| SsoLoginCredentials
|
||||
| UserApiLoginCredentials
|
||||
| AuthRequestLoginCredentials
|
||||
| WebAuthnLoginCredentials
|
||||
| PasswordHashLoginCredentials
|
||||
| OpaqueLoginCredentials;
|
||||
|
||||
/**
|
||||
* Represents email and master password login credentials.
|
||||
* This is not used directly by a LogInStrategy, rather it is transformed into a more specific
|
||||
* PasswordHashLoginCredentials or OpaqueLoginCredentials depending on which strategy is to be used.
|
||||
*/
|
||||
export class PasswordLoginCredentials {
|
||||
readonly type = AuthenticationType.Password;
|
||||
|
||||
@@ -18,6 +36,22 @@ export class PasswordLoginCredentials {
|
||||
public captchaToken?: string,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
) {}
|
||||
|
||||
toSpecificLoginCredentials(
|
||||
preLoginResponse: PrePasswordLoginResponse,
|
||||
): PasswordHashLoginCredentials | OpaqueLoginCredentials {
|
||||
return preLoginResponse.opaqueConfiguration
|
||||
? new OpaqueLoginCredentials(
|
||||
this.email,
|
||||
this.masterPassword,
|
||||
preLoginResponse.opaqueConfiguration,
|
||||
)
|
||||
: new PasswordHashLoginCredentials(
|
||||
this.email,
|
||||
this.masterPassword,
|
||||
preLoginResponse.toKdfConfig(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class SsoLoginCredentials {
|
||||
@@ -104,3 +138,27 @@ export class WebAuthnLoginCredentials {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordHashLoginCredentials {
|
||||
readonly type = AuthenticationType.PasswordHash;
|
||||
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPassword: string,
|
||||
public kdfConfig: KdfConfig,
|
||||
// TODO: PM-15162 - captcha is deprecated as part of UI refresh work
|
||||
public captchaToken?: string,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class OpaqueLoginCredentials {
|
||||
readonly type = AuthenticationType.Opaque;
|
||||
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPassword: string,
|
||||
public cipherConfiguration: CipherConfiguration,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogin.response";
|
||||
import { PrePasswordLoginResponse } from "@bitwarden/common/auth/models/response/pre-password-login.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
@@ -48,6 +49,8 @@ import { UserDecryptionOptionsService } from "../user-decryption-options/user-de
|
||||
import { LoginStrategyService } from "./login-strategy.service";
|
||||
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
|
||||
|
||||
// TODO: update tests to pass
|
||||
// TODO: test makePrePasswordLoginMasterKey
|
||||
describe("LoginStrategyService", () => {
|
||||
let sut: LoginStrategyService;
|
||||
|
||||
@@ -75,6 +78,7 @@ describe("LoginStrategyService", () => {
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let taskSchedulerService: MockProxy<TaskSchedulerService>;
|
||||
let prePasswordLoginApiService: MockProxy<PrePasswordLoginApiService>;
|
||||
|
||||
let stateProvider: FakeGlobalStateProvider;
|
||||
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
|
||||
@@ -107,6 +111,7 @@ describe("LoginStrategyService", () => {
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
taskSchedulerService = mock<TaskSchedulerService>();
|
||||
prePasswordLoginApiService = mock<PrePasswordLoginApiService>();
|
||||
|
||||
sut = new LoginStrategyService(
|
||||
accountService,
|
||||
@@ -134,6 +139,7 @@ describe("LoginStrategyService", () => {
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
taskSchedulerService,
|
||||
prePasswordLoginApiService,
|
||||
);
|
||||
|
||||
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
|
||||
@@ -173,8 +179,8 @@ describe("LoginStrategyService", () => {
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
);
|
||||
apiService.postPrelogin.mockResolvedValue(
|
||||
new PreloginResponse({
|
||||
prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue(
|
||||
new PrePasswordLoginResponse({
|
||||
Kdf: KdfType.Argon2id,
|
||||
KdfIterations: 2,
|
||||
KdfMemory: 16,
|
||||
@@ -207,8 +213,8 @@ describe("LoginStrategyService", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
apiService.postPrelogin.mockResolvedValue(
|
||||
new PreloginResponse({
|
||||
prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue(
|
||||
new PrePasswordLoginResponse({
|
||||
Kdf: KdfType.Argon2id,
|
||||
KdfIterations: 2,
|
||||
KdfMemory: 16,
|
||||
@@ -266,8 +272,8 @@ describe("LoginStrategyService", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
apiService.postPrelogin.mockResolvedValue(
|
||||
new PreloginResponse({
|
||||
prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue(
|
||||
new PrePasswordLoginResponse({
|
||||
Kdf: KdfType.Argon2id,
|
||||
KdfIterations: 2,
|
||||
KdfMemory: 16,
|
||||
@@ -305,8 +311,8 @@ describe("LoginStrategyService", () => {
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
);
|
||||
apiService.postPrelogin.mockResolvedValue(
|
||||
new PreloginResponse({
|
||||
prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue(
|
||||
new PrePasswordLoginResponse({
|
||||
Kdf: KdfType.PBKDF2_SHA256,
|
||||
KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1,
|
||||
}),
|
||||
@@ -329,8 +335,8 @@ describe("LoginStrategyService", () => {
|
||||
const deviceVerificationOtp = "123456";
|
||||
|
||||
// Setup initial login and device verification response
|
||||
apiService.postPrelogin.mockResolvedValue(
|
||||
new PreloginResponse({
|
||||
prePasswordLoginApiService.postPrePasswordLogin.mockResolvedValue(
|
||||
new PrePasswordLoginResponse({
|
||||
Kdf: KdfType.Argon2id,
|
||||
KdfIterations: 2,
|
||||
KdfMemory: 16,
|
||||
|
||||
@@ -20,10 +20,11 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { PrePasswordLoginRequest } from "@bitwarden/common/auth/models/request/pre-password-login.request";
|
||||
import { PrePasswordLoginApiService } from "@bitwarden/common/auth/services/pre-password-login-api.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -36,14 +37,7 @@ import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/plat
|
||||
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
KdfType,
|
||||
KeyService,
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
KdfConfigService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { KeyService, KdfConfig, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||
@@ -51,7 +45,11 @@ import {
|
||||
AuthRequestLoginStrategy,
|
||||
AuthRequestLoginStrategyData,
|
||||
} from "../../login-strategies/auth-request-login.strategy";
|
||||
import { LoginStrategy } from "../../login-strategies/login.strategy";
|
||||
import { BaseLoginStrategy } from "../../login-strategies/base-login.strategy";
|
||||
import {
|
||||
OpaqueLoginStrategy,
|
||||
OpaqueLoginStrategyData,
|
||||
} from "../../login-strategies/opaque-login.strategy";
|
||||
import {
|
||||
PasswordLoginStrategy,
|
||||
PasswordLoginStrategyData,
|
||||
@@ -71,6 +69,7 @@ import {
|
||||
SsoLoginCredentials,
|
||||
AuthRequestLoginCredentials,
|
||||
WebAuthnLoginCredentials,
|
||||
LoginCredentials,
|
||||
} from "../../models";
|
||||
|
||||
import {
|
||||
@@ -83,6 +82,14 @@ import {
|
||||
|
||||
const sessionTimeoutLength = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
type LoginStrategy =
|
||||
| UserApiLoginStrategy
|
||||
| PasswordLoginStrategy
|
||||
| SsoLoginStrategy
|
||||
| AuthRequestLoginStrategy
|
||||
| WebAuthnLoginStrategy
|
||||
| OpaqueLoginStrategy;
|
||||
|
||||
export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
private sessionTimeoutSubscription: Subscription | undefined;
|
||||
private currentAuthnTypeState: GlobalState<AuthenticationType | null>;
|
||||
@@ -94,14 +101,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
authenticationSessionTimeout$: Observable<boolean> =
|
||||
this.authenticationTimeoutSubject.asObservable();
|
||||
|
||||
private loginStrategy$: Observable<
|
||||
| UserApiLoginStrategy
|
||||
| PasswordLoginStrategy
|
||||
| SsoLoginStrategy
|
||||
| AuthRequestLoginStrategy
|
||||
| WebAuthnLoginStrategy
|
||||
| null
|
||||
>;
|
||||
private loginStrategy$: Observable<LoginStrategy | null>;
|
||||
|
||||
currentAuthType$: Observable<AuthenticationType | null>;
|
||||
|
||||
@@ -131,6 +131,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected taskSchedulerService: TaskSchedulerService,
|
||||
protected prePasswordLoginApiService: PrePasswordLoginApiService,
|
||||
) {
|
||||
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
|
||||
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
|
||||
@@ -154,7 +155,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe(
|
||||
distinctUntilChanged(),
|
||||
combineLatestWith(this.loginStrategyCacheState.state$),
|
||||
this.initializeLoginStrategy.bind(this),
|
||||
map(([strategy, data]) => this.initializeLoginStrategy(strategy, data)),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
}
|
||||
@@ -215,16 +216,27 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
await this.clearCache();
|
||||
this.authenticationTimeoutSubject.next(false);
|
||||
|
||||
await this.currentAuthnTypeState.update((_) => credentials.type);
|
||||
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
// Note: We aren't passing the credentials directly to the strategy since they are
|
||||
// created in the popup and can cause DeadObject references on Firefox.
|
||||
// This is a shallow copy, but use deep copy in future if objects are added to credentials
|
||||
// that were created in popup.
|
||||
// If the popup uses its own instance of this service, this can be removed.
|
||||
const ownedCredentials = { ...credentials };
|
||||
let ownedCredentials: LoginCredentials;
|
||||
|
||||
// Password credentials may use the PasswordHashLoginStrategy or the OpaqueLoginStrategy
|
||||
if (credentials.type === AuthenticationType.Password) {
|
||||
const preLoginRequest = new PrePasswordLoginRequest(credentials.email);
|
||||
const preLoginResponse =
|
||||
await this.prePasswordLoginApiService.postPrePasswordLogin(preLoginRequest);
|
||||
ownedCredentials = credentials.toSpecificLoginCredentials(preLoginResponse);
|
||||
} else {
|
||||
// Shallow copy
|
||||
ownedCredentials = { ...credentials };
|
||||
}
|
||||
|
||||
await this.currentAuthnTypeState.update((_) => credentials.type);
|
||||
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
const result = await strategy?.logIn(ownedCredentials as any);
|
||||
|
||||
@@ -309,33 +321,33 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
|
||||
async makePrePasswordLoginMasterKey(
|
||||
masterPassword: string,
|
||||
email: string,
|
||||
kdfConfig?: KdfConfig,
|
||||
): Promise<MasterKey> {
|
||||
email = email.trim().toLowerCase();
|
||||
let kdfConfig: KdfConfig | undefined;
|
||||
try {
|
||||
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
|
||||
if (preloginResponse != null) {
|
||||
kdfConfig =
|
||||
preloginResponse.kdf === KdfType.PBKDF2_SHA256
|
||||
? new PBKDF2KdfConfig(preloginResponse.kdfIterations)
|
||||
: new Argon2KdfConfig(
|
||||
preloginResponse.kdfIterations,
|
||||
preloginResponse.kdfMemory,
|
||||
preloginResponse.kdfParallelism,
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e == null || e.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!kdfConfig) {
|
||||
throw new Error("KDF config is required");
|
||||
}
|
||||
kdfConfig.validateKdfConfigForPrelogin();
|
||||
try {
|
||||
const preloginResponse = await this.prePasswordLoginApiService.postPrePasswordLogin(
|
||||
new PrePasswordLoginRequest(email),
|
||||
);
|
||||
kdfConfig = preloginResponse?.toKdfConfig();
|
||||
} catch (e: any) {
|
||||
if (e == null || e.statusCode !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig);
|
||||
if (!kdfConfig) {
|
||||
throw new Error("KDF config is required");
|
||||
}
|
||||
}
|
||||
|
||||
kdfConfig.validateKdfConfigForPreLogin();
|
||||
|
||||
return this.keyService.makeMasterKey(masterPassword, email, kdfConfig);
|
||||
}
|
||||
|
||||
private async clearCache(): Promise<void> {
|
||||
@@ -383,9 +395,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
}
|
||||
|
||||
private initializeLoginStrategy(
|
||||
source: Observable<[AuthenticationType | null, CacheData | null]>,
|
||||
) {
|
||||
const sharedDeps: ConstructorParameters<typeof LoginStrategy> = [
|
||||
strategy: AuthenticationType | null,
|
||||
data: CacheData | null,
|
||||
): LoginStrategy | null {
|
||||
const sharedDeps: ConstructorParameters<typeof BaseLoginStrategy> = [
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
@@ -404,49 +417,51 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
this.kdfConfigService,
|
||||
this.environmentService,
|
||||
];
|
||||
|
||||
return source.pipe(
|
||||
map(([strategy, data]) => {
|
||||
if (strategy == null) {
|
||||
return null;
|
||||
}
|
||||
switch (strategy) {
|
||||
case AuthenticationType.Password:
|
||||
return new PasswordLoginStrategy(
|
||||
data?.password ?? new PasswordLoginStrategyData(),
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.Sso:
|
||||
return new SsoLoginStrategy(
|
||||
data?.sso ?? new SsoLoginStrategyData(),
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustService,
|
||||
this.authRequestService,
|
||||
this.i18nService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.UserApiKey:
|
||||
return new UserApiLoginStrategy(
|
||||
data?.userApiKey ?? new UserApiLoginStrategyData(),
|
||||
this.keyConnectorService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.AuthRequest:
|
||||
return new AuthRequestLoginStrategy(
|
||||
data?.authRequest ?? new AuthRequestLoginStrategyData(),
|
||||
this.deviceTrustService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.WebAuthn:
|
||||
return new WebAuthnLoginStrategy(
|
||||
data?.webAuthn ?? new WebAuthnLoginStrategyData(),
|
||||
...sharedDeps,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
switch (strategy) {
|
||||
case AuthenticationType.PasswordHash:
|
||||
return new PasswordLoginStrategy(
|
||||
data?.password ?? new PasswordLoginStrategyData(),
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.Sso:
|
||||
return new SsoLoginStrategy(
|
||||
data?.sso ?? new SsoLoginStrategyData(),
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustService,
|
||||
this.authRequestService,
|
||||
this.i18nService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.UserApiKey:
|
||||
return new UserApiLoginStrategy(
|
||||
data?.userApiKey ?? new UserApiLoginStrategyData(),
|
||||
this.keyConnectorService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.AuthRequest:
|
||||
return new AuthRequestLoginStrategy(
|
||||
data?.authRequest ?? new AuthRequestLoginStrategyData(),
|
||||
this.deviceTrustService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.WebAuthn:
|
||||
return new WebAuthnLoginStrategy(
|
||||
data?.webAuthn ?? new WebAuthnLoginStrategyData(),
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.Opaque:
|
||||
return new OpaqueLoginStrategy(
|
||||
data?.opaque ?? new OpaqueLoginStrategyData(),
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
...sharedDeps,
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
||||
import { KeyDefinition, LOGIN_STRATEGY_MEMORY } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy";
|
||||
import { OpaqueLoginStrategyData } from "../../login-strategies/opaque-login.strategy";
|
||||
import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy";
|
||||
import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy";
|
||||
import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy";
|
||||
@@ -48,6 +49,7 @@ export const AUTH_REQUEST_PUSH_NOTIFICATION_KEY = new KeyDefinition<string | nul
|
||||
|
||||
export type CacheData = {
|
||||
password?: PasswordLoginStrategyData;
|
||||
opaque?: OpaqueLoginStrategyData;
|
||||
sso?: SsoLoginStrategyData;
|
||||
userApiKey?: UserApiLoginStrategyData;
|
||||
authRequest?: AuthRequestLoginStrategyData;
|
||||
@@ -68,6 +70,7 @@ export const CACHE_KEY = new KeyDefinition<CacheData | null>(
|
||||
}
|
||||
return {
|
||||
password: data.password ? PasswordLoginStrategyData.fromJSON(data.password) : undefined,
|
||||
opaque: data.opaque ? OpaqueLoginStrategyData.fromJSON(data.opaque) : undefined,
|
||||
sso: data.sso ? SsoLoginStrategyData.fromJSON(data.sso) : undefined,
|
||||
userApiKey: data.userApiKey
|
||||
? UserApiLoginStrategyData.fromJSON(data.userApiKey)
|
||||
|
||||
@@ -43,6 +43,7 @@ import { DeviceVerificationRequest } from "../auth/models/request/device-verific
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "../auth/models/request/email.request";
|
||||
import { OpaqueTokenRequest } from "../auth/models/request/identity-token/opaque-token.request";
|
||||
import { PasswordTokenRequest } from "../auth/models/request/identity-token/password-token.request";
|
||||
import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token.request";
|
||||
import { UserApiTokenRequest } from "../auth/models/request/identity-token/user-api-token.request";
|
||||
@@ -74,7 +75,6 @@ import { IdentityDeviceVerificationResponse } from "../auth/models/response/iden
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
import { PreloginResponse } from "../auth/models/response/prelogin.response";
|
||||
import { RegisterResponse } from "../auth/models/response/register.response";
|
||||
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
|
||||
import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response";
|
||||
@@ -100,7 +100,6 @@ import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
import { KeysRequest } from "../models/request/keys.request";
|
||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||
import { RegisterRequest } from "../models/request/register.request";
|
||||
import { StorageRequest } from "../models/request/storage.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
@@ -151,7 +150,8 @@ export abstract class ApiService {
|
||||
| PasswordTokenRequest
|
||||
| SsoTokenRequest
|
||||
| UserApiTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
| WebAuthnLoginTokenRequest
|
||||
| OpaqueTokenRequest,
|
||||
) => Promise<
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
@@ -166,7 +166,6 @@ export abstract class ApiService {
|
||||
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
|
||||
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
|
||||
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
|
||||
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
|
||||
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
|
||||
postEmail: (request: EmailRequest) => Promise<any>;
|
||||
postPassword: (request: PasswordRequest) => Promise<any>;
|
||||
|
||||
@@ -4,4 +4,6 @@ export enum AuthenticationType {
|
||||
UserApiKey = 2,
|
||||
AuthRequest = 3,
|
||||
WebAuthn = 4,
|
||||
PasswordHash = 5,
|
||||
Opaque = 6,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ClientType } from "../../../../enums";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
|
||||
import { DeviceRequest } from "./device.request";
|
||||
import { TokenTwoFactorRequest } from "./token-two-factor.request";
|
||||
import { TokenRequest } from "./token.request";
|
||||
|
||||
// TODO: we might have to support both login start and login finish requests within this?
|
||||
export class OpaqueTokenRequest extends TokenRequest {
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPasswordHash: string,
|
||||
protected twoFactor: TokenTwoFactorRequest,
|
||||
device?: DeviceRequest,
|
||||
public newDeviceOtp?: string,
|
||||
) {
|
||||
super(twoFactor, device);
|
||||
}
|
||||
|
||||
toIdentityToken(clientId: ClientType) {
|
||||
const obj = super.toIdentityToken(clientId);
|
||||
|
||||
obj.grant_type = "password";
|
||||
obj.username = this.email;
|
||||
obj.password = this.masterPasswordHash;
|
||||
|
||||
if (this.newDeviceOtp) {
|
||||
obj.newDeviceOtp = this.newDeviceOtp;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
return Object.assign(Object.create(OpaqueTokenRequest.prototype), json, {
|
||||
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||
twoFactor: json.twoFactor
|
||||
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export class PreloginRequest {
|
||||
export class PrePasswordLoginRequest {
|
||||
email: string;
|
||||
|
||||
constructor(email: string) {
|
||||
@@ -1,9 +1,9 @@
|
||||
import { KdfType } from "@bitwarden/key-management";
|
||||
import { KdfType, createKdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherConfiguration } from "../../opaque/models/cipher-configuration";
|
||||
|
||||
export class PreloginResponse extends BaseResponse {
|
||||
export class PrePasswordLoginResponse extends BaseResponse {
|
||||
kdf: KdfType;
|
||||
kdfIterations: number;
|
||||
kdfMemory?: number;
|
||||
@@ -19,4 +19,8 @@ export class PreloginResponse extends BaseResponse {
|
||||
this.kdfParallelism = this.getResponseProperty("KdfParallelism");
|
||||
this.opaqueConfiguration = this.getResponseProperty("OpaqueConfiguration");
|
||||
}
|
||||
|
||||
toKdfConfig() {
|
||||
return createKdfConfig(this);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
import { LoginFinishRequest } from "./models/login-finish.request";
|
||||
import { LoginStartRequest } from "./models/login-start.request";
|
||||
import { LoginStartResponse } from "./models/login-start.response";
|
||||
import { RegistrationFinishRequest } from "./models/registration-finish.request";
|
||||
import { RegistrationFinishResponse } from "./models/registration-finish.response";
|
||||
import { RegistrationStartRequest } from "./models/registration-start.request";
|
||||
import { RegistrationStartResponse } from "./models/registration-start.response";
|
||||
import { OpaqueApiService } from "./opaque-api.service";
|
||||
|
||||
export class DefaultOpaqueApiService implements OpaqueApiService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationStartResponse(response);
|
||||
}
|
||||
|
||||
async registrationFinish(
|
||||
request: RegistrationFinishRequest,
|
||||
): Promise<RegistrationFinishResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationFinishResponse(response);
|
||||
}
|
||||
|
||||
async loginStart(request: LoginStartRequest): Promise<LoginStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new LoginStartResponse(response);
|
||||
}
|
||||
|
||||
async loginFinish(request: LoginFinishRequest): Promise<boolean> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return response.success;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
import { LoginFinishRequest } from "./models/login-finish.request";
|
||||
import { LoginStartRequest } from "./models/login-start.request";
|
||||
import { LoginStartResponse } from "./models/login-start.response";
|
||||
@@ -6,11 +11,63 @@ import { RegistrationFinishResponse } from "./models/registration-finish.respons
|
||||
import { RegistrationStartRequest } from "./models/registration-start.request";
|
||||
import { RegistrationStartResponse } from "./models/registration-start.response";
|
||||
|
||||
export abstract class OpaqueApiService {
|
||||
abstract registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse>;
|
||||
abstract registrationFinish(
|
||||
export class OpaqueApiService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationStartResponse(response);
|
||||
}
|
||||
|
||||
async registrationFinish(
|
||||
request: RegistrationFinishRequest,
|
||||
): Promise<RegistrationFinishResponse>;
|
||||
abstract loginStart(request: LoginStartRequest): Promise<LoginStartResponse>;
|
||||
abstract loginFinish(request: LoginFinishRequest): Promise<boolean>;
|
||||
): Promise<RegistrationFinishResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationFinishResponse(response);
|
||||
}
|
||||
|
||||
async loginStart(request: LoginStartRequest): Promise<LoginStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new LoginStartResponse(response);
|
||||
}
|
||||
|
||||
async loginFinish(request: LoginFinishRequest): Promise<boolean> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return response.success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
import { PrePasswordLoginRequest } from "../models/request/pre-password-login.request";
|
||||
import { PrePasswordLoginResponse } from "../models/response/pre-password-login.response";
|
||||
|
||||
/**
|
||||
* An API service which facilitates retrieving key derivation information
|
||||
* required for password-based login before the user has authenticated.
|
||||
*/
|
||||
export class PrePasswordLoginApiService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async postPrePasswordLogin(request: PrePasswordLoginRequest): Promise<PrePasswordLoginResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/prelogin",
|
||||
request,
|
||||
false,
|
||||
true,
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new PrePasswordLoginResponse(r);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ import { IdentityDeviceVerificationResponse } from "../auth/models/response/iden
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
import { PreloginResponse } from "../auth/models/response/prelogin.response";
|
||||
import { RegisterResponse } from "../auth/models/response/register.response";
|
||||
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
|
||||
import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response";
|
||||
@@ -111,7 +110,6 @@ import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
import { KeysRequest } from "../models/request/keys.request";
|
||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||
import { RegisterRequest } from "../models/request/register.request";
|
||||
import { StorageRequest } from "../models/request/storage.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
@@ -353,19 +351,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return this.send("PUT", "/accounts/tax", request, true, false);
|
||||
}
|
||||
|
||||
async postPrelogin(request: PreloginRequest): Promise<PreloginResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.send(
|
||||
"POST",
|
||||
"/accounts/prelogin",
|
||||
request,
|
||||
false,
|
||||
true,
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new PreloginResponse(r);
|
||||
}
|
||||
|
||||
postEmailToken(request: EmailTokenRequest): Promise<any> {
|
||||
return this.send("POST", "/accounts/email-token", request, true, false);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
PBKDF2KdfConfig,
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
createKdfConfig,
|
||||
DEFAULT_KDF_CONFIG,
|
||||
} from "./models/kdf-config";
|
||||
export { KdfConfigService } from "./abstractions/kdf-config.service";
|
||||
|
||||
@@ -34,19 +34,19 @@ describe("KdfConfig", () => {
|
||||
|
||||
it("validateKdfConfigForPrelogin(): should validate the PBKDF2 KDF config", () => {
|
||||
const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow();
|
||||
expect(() => kdfConfig.validateKdfConfigForPreLogin()).not.toThrow();
|
||||
});
|
||||
|
||||
it("validateKdfConfigForPrelogin(): should validate the Argon2id KDF config", () => {
|
||||
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4);
|
||||
expect(() => kdfConfig.validateKdfConfigForPrelogin()).not.toThrow();
|
||||
expect(() => kdfConfig.validateKdfConfigForPreLogin()).not.toThrow();
|
||||
});
|
||||
|
||||
it("validateKdfConfigForPrelogin(): should throw an error for too low PBKDF2 iterations", () => {
|
||||
const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(
|
||||
PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1,
|
||||
);
|
||||
expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow(
|
||||
expect(() => kdfConfig.validateKdfConfigForPreLogin()).toThrow(
|
||||
`PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`,
|
||||
);
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe("KdfConfig", () => {
|
||||
64,
|
||||
4,
|
||||
);
|
||||
expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow(
|
||||
expect(() => kdfConfig.validateKdfConfigForPreLogin()).toThrow(
|
||||
`Argon2 iterations must be at least ${Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${kdfConfig.iterations}; possible pre-login downgrade attack detected.`,
|
||||
);
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe("KdfConfig", () => {
|
||||
Argon2KdfConfig.PRELOGIN_MEMORY_MIN - 1,
|
||||
4,
|
||||
);
|
||||
expect(() => kdfConfig.validateKdfConfigForPrelogin()).toThrow(
|
||||
expect(() => kdfConfig.validateKdfConfigForPreLogin()).toThrow(
|
||||
`Argon2 memory must be at least ${Argon2KdfConfig.PRELOGIN_MEMORY_MIN} MiB, but was ${kdfConfig.memory} MiB; possible pre-login downgrade attack detected.`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,20 @@ import { KdfType } from "../enums/kdf-type.enum";
|
||||
*/
|
||||
export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig;
|
||||
|
||||
/**
|
||||
* A factory function that instantiates a new KdfConfig from an object that may represent one of several KdfTypes.
|
||||
* This is useful for instantiating the correct KdfConfig from a server response object.
|
||||
*/
|
||||
export const createKdfConfig = (obj: {
|
||||
kdf: KdfType;
|
||||
kdfIterations: number;
|
||||
kdfMemory?: number;
|
||||
kdfParallelism?: number;
|
||||
}): KdfConfig =>
|
||||
obj.kdf === KdfType.PBKDF2_SHA256
|
||||
? new PBKDF2KdfConfig(obj.kdfIterations)
|
||||
: new Argon2KdfConfig(obj.kdfIterations, obj.kdfMemory, obj.kdfParallelism);
|
||||
|
||||
/**
|
||||
* Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration.
|
||||
*/
|
||||
@@ -38,7 +52,7 @@ export class PBKDF2KdfConfig {
|
||||
* Validates the PBKDF2 KDF configuration for pre-login.
|
||||
* A Valid PBKDF2 KDF configuration has KDF iterations between the 5000 and 2_000_000.
|
||||
*/
|
||||
validateKdfConfigForPrelogin(): void {
|
||||
validateKdfConfigForPreLogin(): void {
|
||||
if (PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN > this.iterations) {
|
||||
throw new Error(
|
||||
`PBKDF2 iterations must be at least ${PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${this.iterations}; possible pre-login downgrade attack detected.`,
|
||||
@@ -101,7 +115,7 @@ export class Argon2KdfConfig {
|
||||
/**
|
||||
* Validates the Argon2 KDF configuration for pre-login.
|
||||
*/
|
||||
validateKdfConfigForPrelogin(): void {
|
||||
validateKdfConfigForPreLogin(): void {
|
||||
if (Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN > this.iterations) {
|
||||
throw new Error(
|
||||
`Argon2 iterations must be at least ${Argon2KdfConfig.PRELOGIN_ITERATIONS_MIN}, but was ${this.iterations}; possible pre-login downgrade attack detected.`,
|
||||
|
||||
Reference in New Issue
Block a user