mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-22 02:21:34 +01: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:
parent
6b1da67f3a
commit
a0e0637bb6
@ -26,6 +26,10 @@ import {
|
||||
factory,
|
||||
FactoryOptions,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
globalStateProviderFactory,
|
||||
GlobalStateProviderInitOptions,
|
||||
} from "../../../platform/background/service-factories/global-state-provider.factory";
|
||||
import {
|
||||
i18nServiceFactory,
|
||||
I18nServiceInitOptions,
|
||||
@ -84,7 +88,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
|
||||
PolicyServiceInitOptions &
|
||||
PasswordStrengthServiceInitOptions &
|
||||
DeviceTrustCryptoServiceInitOptions &
|
||||
AuthRequestServiceInitOptions;
|
||||
AuthRequestServiceInitOptions &
|
||||
GlobalStateProviderInitOptions;
|
||||
|
||||
export function loginStrategyServiceFactory(
|
||||
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
|
||||
@ -113,6 +118,7 @@ export function loginStrategyServiceFactory(
|
||||
await policyServiceFactory(cache, opts),
|
||||
await deviceTrustCryptoServiceFactory(cache, opts),
|
||||
await authRequestServiceFactory(cache, opts),
|
||||
await globalStateProviderFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -571,6 +571,7 @@ export default class MainBackground {
|
||||
this.policyService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
this.globalStateProvider,
|
||||
);
|
||||
|
||||
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
||||
|
@ -273,8 +273,8 @@ export class LoginCommand {
|
||||
selectedProvider.type === TwoFactorProviderType.Email
|
||||
) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = this.loginStrategyService.email;
|
||||
emailReq.masterPasswordHash = this.loginStrategyService.masterPasswordHash;
|
||||
emailReq.email = await this.loginStrategyService.getEmail();
|
||||
emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
|
||||
|
@ -458,6 +458,7 @@ export class Main {
|
||||
this.policyService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
this.globalStateProvider,
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
|
@ -99,8 +99,7 @@ export class LoginViaAuthRequestComponent
|
||||
}
|
||||
|
||||
//gets signalR push notification
|
||||
this.loginStrategyService
|
||||
.getPushNotificationObs$()
|
||||
this.loginStrategyService.authRequestPushNotification$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => {
|
||||
// Only fires on approval currently
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Directive, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import * as DuoWebSDK from "duo_web_sdk";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@ -10,6 +11,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
@ -92,7 +94,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.authing || this.twoFactorService.getProviders() == null) {
|
||||
if (!(await this.authing()) || this.twoFactorService.getProviders() == null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.loginRoute]);
|
||||
@ -105,7 +107,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
});
|
||||
|
||||
if (this.needsLock) {
|
||||
if (await this.needsLock()) {
|
||||
this.successRoute = "lock";
|
||||
}
|
||||
|
||||
@ -426,7 +428,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.loginStrategyService.email == null) {
|
||||
if ((await this.loginStrategyService.getEmail()) == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
@ -437,12 +439,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
|
||||
try {
|
||||
const request = new TwoFactorEmailRequest();
|
||||
request.email = this.loginStrategyService.email;
|
||||
request.masterPasswordHash = this.loginStrategyService.masterPasswordHash;
|
||||
request.ssoEmail2FaSessionToken = this.loginStrategyService.ssoEmail2FaSessionToken;
|
||||
request.email = await this.loginStrategyService.getEmail();
|
||||
request.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||
request.ssoEmail2FaSessionToken =
|
||||
await this.loginStrategyService.getSsoEmail2FaSessionToken();
|
||||
request.deviceIdentifier = await this.appIdService.getAppId();
|
||||
request.authRequestAccessCode = this.loginStrategyService.accessCode;
|
||||
request.authRequestId = this.loginStrategyService.authRequestId;
|
||||
request.authRequestAccessCode = await this.loginStrategyService.getAccessCode();
|
||||
request.authRequestId = await this.loginStrategyService.getAuthRequestId();
|
||||
this.emailPromise = this.apiService.postTwoFactorEmail(request);
|
||||
await this.emailPromise;
|
||||
if (doToast) {
|
||||
@ -476,20 +479,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
}
|
||||
|
||||
get authing(): boolean {
|
||||
return (
|
||||
this.loginStrategyService.authingWithPassword() ||
|
||||
this.loginStrategyService.authingWithSso() ||
|
||||
this.loginStrategyService.authingWithUserApiKey() ||
|
||||
this.loginStrategyService.authingWithPasswordless()
|
||||
);
|
||||
private async authing(): Promise<boolean> {
|
||||
return (await firstValueFrom(this.loginStrategyService.currentAuthType$)) !== null;
|
||||
}
|
||||
|
||||
get needsLock(): boolean {
|
||||
return (
|
||||
this.loginStrategyService.authingWithSso() ||
|
||||
this.loginStrategyService.authingWithUserApiKey()
|
||||
);
|
||||
private async needsLock(): Promise<boolean> {
|
||||
const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$);
|
||||
return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey;
|
||||
}
|
||||
|
||||
// implemented in clients
|
||||
|
@ -328,6 +328,7 @@ import { ModalService } from "./modal.service";
|
||||
PolicyServiceAbstraction,
|
||||
DeviceTrustCryptoServiceAbstraction,
|
||||
AuthRequestServiceAbstraction,
|
||||
GlobalStateProvider,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -10,7 +10,11 @@ module.exports = {
|
||||
displayName: "libs/auth tests",
|
||||
preset: "jest-preset-angular",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
moduleNameMapper: pathsToModuleNameMapper(
|
||||
// lets us use @bitwarden/common/spec in tests
|
||||
{ "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||
{
|
||||
prefix: "<rootDir>/",
|
||||
},
|
||||
),
|
||||
};
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
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 { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
@ -14,12 +16,45 @@ import {
|
||||
} from "../models/domain/login-credentials";
|
||||
|
||||
export abstract class LoginStrategyServiceAbstraction {
|
||||
masterPasswordHash: string;
|
||||
email: string;
|
||||
accessCode: string;
|
||||
authRequestId: string;
|
||||
ssoEmail2FaSessionToken: string;
|
||||
/**
|
||||
* The current strategy being used to authenticate.
|
||||
* Emits null if the session has timed out.
|
||||
*/
|
||||
currentAuthType$: Observable<AuthenticationType | null>;
|
||||
/**
|
||||
* Emits when an auth request has been approved.
|
||||
*/
|
||||
authRequestPushNotification$: Observable<string>;
|
||||
/**
|
||||
* If the login strategy uses the email address of the user, this
|
||||
* will return it. Otherwise, it will return null.
|
||||
*/
|
||||
getEmail: () => Promise<string | null>;
|
||||
/**
|
||||
* If the user is logging in with a master password, this will return
|
||||
* the master password hash. Otherwise, it will return null.
|
||||
*/
|
||||
getMasterPasswordHash: () => Promise<string | null>;
|
||||
/**
|
||||
* If the user is logging in with SSO, this will return
|
||||
* the email auth token. Otherwise, it will return null.
|
||||
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
|
||||
*/
|
||||
getSsoEmail2FaSessionToken: () => Promise<string | null>;
|
||||
/**
|
||||
* Returns the access code if the user is logging in with an
|
||||
* Auth Request. Otherwise, it will return null.
|
||||
*/
|
||||
getAccessCode: () => Promise<string | null>;
|
||||
/**
|
||||
* Returns the auth request ID if the user is logging in with an
|
||||
* Auth Request. Otherwise, it will return null.
|
||||
*/
|
||||
getAuthRequestId: () => Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Sends a token request to the server using the provided credentials.
|
||||
*/
|
||||
logIn: (
|
||||
credentials:
|
||||
| UserApiLoginCredentials
|
||||
@ -28,15 +63,30 @@ export abstract class LoginStrategyServiceAbstraction {
|
||||
| AuthRequestLoginCredentials
|
||||
| WebAuthnLoginCredentials,
|
||||
) => Promise<AuthResult>;
|
||||
/**
|
||||
* Sends a token request to the server with the provided two factor token
|
||||
* and captcha response. This uses data stored from {@link LoginStrategyServiceAbstraction.logIn},
|
||||
* so that must be called first.
|
||||
* Returns an error if no session data is found.
|
||||
*/
|
||||
logInTwoFactor: (
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
) => Promise<AuthResult>;
|
||||
/**
|
||||
* Creates a master key from the provided master password and email.
|
||||
*/
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||
authingWithUserApiKey: () => boolean;
|
||||
authingWithSso: () => boolean;
|
||||
authingWithPassword: () => boolean;
|
||||
authingWithPasswordless: () => boolean;
|
||||
authResponsePushNotification: (notification: AuthRequestPushNotification) => Promise<any>;
|
||||
getPushNotificationObs$: () => Observable<any>;
|
||||
/**
|
||||
* Sends a notification to {@link LoginStrategyServiceAbstraction.authRequestPushNotification}
|
||||
*/
|
||||
sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => Promise<void>;
|
||||
/**
|
||||
* Sends a response to an auth request.
|
||||
*/
|
||||
passwordlessLogin: (
|
||||
id: string,
|
||||
key: string,
|
||||
requestApproved: boolean,
|
||||
) => Promise<AuthRequestResponse>;
|
||||
}
|
||||
|
@ -18,10 +18,15 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { AuthRequestLoginStrategy } from "./auth-request-login.strategy";
|
||||
import {
|
||||
AuthRequestLoginStrategy,
|
||||
AuthRequestLoginStrategyData,
|
||||
} from "./auth-request-login.strategy";
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
|
||||
describe("AuthRequestLoginStrategy", () => {
|
||||
let cache: AuthRequestLoginStrategyData;
|
||||
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
@ -65,6 +70,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
||||
cache,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
@ -14,26 +17,33 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { AuthRequestLoginCredentials } 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 AuthRequestLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
captchaBypassToken: string;
|
||||
authRequestCredentials: AuthRequestLoginCredentials;
|
||||
|
||||
static fromJSON(obj: Jsonify<AuthRequestLoginStrategyData>): AuthRequestLoginStrategyData {
|
||||
const data = Object.assign(new AuthRequestLoginStrategyData(), obj, {
|
||||
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
|
||||
authRequestCredentials: AuthRequestLoginCredentials.fromJSON(obj.authRequestCredentials),
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
get email() {
|
||||
return this.tokenRequest.email;
|
||||
}
|
||||
email$: Observable<string>;
|
||||
accessCode$: Observable<string>;
|
||||
authRequestId$: Observable<string>;
|
||||
|
||||
get accessCode() {
|
||||
return this.authRequestCredentials.accessCode;
|
||||
}
|
||||
|
||||
get authRequestId() {
|
||||
return this.authRequestCredentials.authRequestId;
|
||||
}
|
||||
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
private authRequestCredentials: AuthRequestLoginCredentials;
|
||||
protected cache: BehaviorSubject<AuthRequestLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: AuthRequestLoginStrategyData,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
@ -56,22 +66,26 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((data) => data.tokenRequest.email));
|
||||
this.accessCode$ = this.cache.pipe(map((data) => data.authRequestCredentials.accessCode));
|
||||
this.authRequestId$ = this.cache.pipe(map((data) => data.authRequestCredentials.authRequestId));
|
||||
}
|
||||
|
||||
override async logIn(credentials: AuthRequestLoginCredentials) {
|
||||
// NOTE: To avoid DeadObject references on Firefox, do not set the credentials object directly
|
||||
// Use deep copy in future if objects are added that were created in popup
|
||||
this.authRequestCredentials = { ...credentials };
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
const data = new AuthRequestLoginStrategyData();
|
||||
data.tokenRequest = new PasswordTokenRequest(
|
||||
credentials.email,
|
||||
credentials.accessCode,
|
||||
null,
|
||||
await this.buildTwoFactor(credentials.twoFactor),
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
data.tokenRequest.setAuthRequestAccessCode(credentials.authRequestId);
|
||||
data.authRequestCredentials = credentials;
|
||||
this.cache.next(data);
|
||||
|
||||
this.tokenRequest.setAuthRequestAccessCode(credentials.authRequestId);
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
@ -80,27 +94,32 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
const data = this.cache.value;
|
||||
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
|
||||
this.cache.next(data);
|
||||
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||
if (
|
||||
this.authRequestCredentials.decryptedMasterKey &&
|
||||
this.authRequestCredentials.decryptedMasterKeyHash
|
||||
authRequestCredentials.decryptedMasterKey &&
|
||||
authRequestCredentials.decryptedMasterKeyHash
|
||||
) {
|
||||
await this.cryptoService.setMasterKey(this.authRequestCredentials.decryptedMasterKey);
|
||||
await this.cryptoService.setMasterKeyHash(this.authRequestCredentials.decryptedMasterKeyHash);
|
||||
await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey);
|
||||
await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||
// User now may or may not have a master password
|
||||
// but set the master key encrypted user key if it exists regardless
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
|
||||
if (this.authRequestCredentials.decryptedUserKey) {
|
||||
await this.cryptoService.setUserKey(this.authRequestCredentials.decryptedUserKey);
|
||||
if (authRequestCredentials.decryptedUserKey) {
|
||||
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
||||
} else {
|
||||
await this.trySetUserKeyWithMasterKey();
|
||||
// Establish trust if required after setting user key
|
||||
@ -121,4 +140,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount()),
|
||||
);
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
authRequest: this.cache.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { PasswordLoginStrategy } from "./password-login.strategy";
|
||||
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
|
||||
|
||||
const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
@ -94,6 +94,8 @@ export function identityTokenResponseFactory(
|
||||
|
||||
// TODO: add tests for latest changes to base class for TDE
|
||||
describe("LoginStrategy", () => {
|
||||
let cache: PasswordLoginStrategyData;
|
||||
|
||||
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
@ -129,6 +131,7 @@ describe("LoginStrategy", () => {
|
||||
|
||||
// The base class is abstract so we test it via PasswordLoginStrategy
|
||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||
cache,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
@ -377,11 +380,23 @@ describe("LoginStrategy", () => {
|
||||
|
||||
it("sends 2FA token provided by user to server (two-step)", async () => {
|
||||
// Simulate a partially completed login
|
||||
passwordLoginStrategy.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
masterPasswordHash,
|
||||
null,
|
||||
null,
|
||||
cache = new PasswordLoginStrategyData();
|
||||
cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null);
|
||||
|
||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||
cache,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
@ -36,16 +38,21 @@ import {
|
||||
AuthRequestLoginCredentials,
|
||||
WebAuthnLoginCredentials,
|
||||
} from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
|
||||
|
||||
export abstract class LoginStrategy {
|
||||
protected abstract tokenRequest:
|
||||
export abstract class LoginStrategyData {
|
||||
tokenRequest:
|
||||
| UserApiTokenRequest
|
||||
| PasswordTokenRequest
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest;
|
||||
protected captchaBypassToken: string = null;
|
||||
captchaBypassToken?: string;
|
||||
}
|
||||
|
||||
export abstract class LoginStrategy {
|
||||
protected abstract cache: BehaviorSubject<LoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
protected cryptoService: CryptoService,
|
||||
@ -59,6 +66,8 @@ export abstract class LoginStrategy {
|
||||
protected twoFactorService: TwoFactorService,
|
||||
) {}
|
||||
|
||||
abstract exportCache(): CacheData;
|
||||
|
||||
abstract logIn(
|
||||
credentials:
|
||||
| UserApiLoginCredentials
|
||||
@ -72,7 +81,9 @@ export abstract class LoginStrategy {
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string = null,
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.setTwoFactor(twoFactor);
|
||||
const data = this.cache.value;
|
||||
data.tokenRequest.setTwoFactor(twoFactor);
|
||||
this.cache.next(data);
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
@ -80,7 +91,8 @@ export abstract class LoginStrategy {
|
||||
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
|
||||
this.twoFactorService.clearSelectedProvider();
|
||||
|
||||
const response = await this.apiService.postIdentityToken(this.tokenRequest);
|
||||
const tokenRequest = this.cache.value.tokenRequest;
|
||||
const response = await this.apiService.postIdentityToken(tokenRequest);
|
||||
|
||||
if (response instanceof IdentityTwoFactorResponse) {
|
||||
return [await this.processTwoFactorResponse(response), response];
|
||||
@ -195,9 +207,7 @@ export abstract class LoginStrategy {
|
||||
|
||||
// The keys comes from different sources depending on the login strategy
|
||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||
@ -221,7 +231,7 @@ export abstract class LoginStrategy {
|
||||
result.twoFactorProviders = response.twoFactorProviders2;
|
||||
|
||||
this.twoFactorService.setProviders(response);
|
||||
this.captchaBypassToken = response.captchaToken ?? null;
|
||||
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
|
||||
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
|
||||
result.email = response.email;
|
||||
return result;
|
||||
|
@ -29,7 +29,7 @@ import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { PasswordLoginStrategy } from "./password-login.strategy";
|
||||
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
|
||||
|
||||
const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
@ -47,6 +47,8 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({
|
||||
});
|
||||
|
||||
describe("PasswordLoginStrategy", () => {
|
||||
let cache: PasswordLoginStrategyData;
|
||||
|
||||
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
@ -93,6 +95,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
policyService.evaluateMasterPassword.mockReturnValue(true);
|
||||
|
||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||
cache,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,11 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@ -25,9 +28,6 @@ import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { SsoLoginStrategy } from "./sso-login.strategy";
|
||||
|
||||
// TODO: Add tests for new trySetUserKeyWithApprovedAdminRequestIfExists logic
|
||||
// https://bitwarden.atlassian.net/browse/PM-3339
|
||||
|
||||
describe("SsoLoginStrategy", () => {
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
@ -74,6 +74,7 @@ describe("SsoLoginStrategy", () => {
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
ssoLoginStrategy = new SsoLoginStrategy(
|
||||
null,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
@ -258,6 +259,114 @@ describe("SsoLoginStrategy", () => {
|
||||
// Assert
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("AdminAuthRequest", () => {
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
tokenResponse = identityTokenResponseFactory(null, {
|
||||
HasMasterPassword: true,
|
||||
TrustedDeviceOption: {
|
||||
HasAdminApproval: true,
|
||||
HasLoginApprovingDevice: false,
|
||||
HasManageResetPasswordPermission: false,
|
||||
EncryptedPrivateKey: mockEncDevicePrivateKey,
|
||||
EncryptedUserKey: mockEncUserKey,
|
||||
},
|
||||
});
|
||||
|
||||
const adminAuthRequest = {
|
||||
id: "1",
|
||||
privateKey: "PRIVATE" as any,
|
||||
} as AdminAuthRequestStorable;
|
||||
stateService.getAdminAuthRequest.mockResolvedValue(
|
||||
new AdminAuthRequestStorable(adminAuthRequest),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the user key using master key and hash from approved admin request if exists", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.hasUserKey.mockResolvedValue(true);
|
||||
const adminAuthResponse = {
|
||||
id: "1",
|
||||
publicKey: "PRIVATE" as any,
|
||||
key: "KEY" as any,
|
||||
masterPasswordHash: "HASH" as any,
|
||||
requestApproved: true,
|
||||
};
|
||||
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled();
|
||||
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets the user key from approved admin request if exists", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.hasUserKey.mockResolvedValue(true);
|
||||
const adminAuthResponse = {
|
||||
id: "1",
|
||||
publicKey: "PRIVATE" as any,
|
||||
key: "KEY" as any,
|
||||
requestApproved: true,
|
||||
};
|
||||
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled();
|
||||
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attempts to establish a trusted device if successful", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.hasUserKey.mockResolvedValue(true);
|
||||
const adminAuthResponse = {
|
||||
id: "1",
|
||||
publicKey: "PRIVATE" as any,
|
||||
key: "KEY" as any,
|
||||
requestApproved: true,
|
||||
};
|
||||
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled();
|
||||
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears the admin auth request if server returns a 404, meaning it was deleted", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
apiService.getAuthRequest.mockRejectedValue(new ErrorResponse(null, 404));
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null);
|
||||
expect(
|
||||
authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled();
|
||||
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attempts to login with a trusted device if admin auth request isn't successful", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
const adminAuthResponse = {
|
||||
id: "1",
|
||||
publicKey: "PRIVATE" as any,
|
||||
key: "KEY" as any,
|
||||
requestApproved: true,
|
||||
};
|
||||
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||
cryptoService.hasUserKey.mockResolvedValue(false);
|
||||
deviceTrustCryptoService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Key Connector", () => {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
@ -19,20 +22,54 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
import { LoginStrategy } from "./login.strategy";
|
||||
import { LoginStrategyData, LoginStrategy } from "./login.strategy";
|
||||
|
||||
export class SsoLoginStrategyData implements LoginStrategyData {
|
||||
captchaBypassToken: string;
|
||||
tokenRequest: SsoTokenRequest;
|
||||
/**
|
||||
* User email address. Only available after authentication.
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* The organization ID that the user is logging into. Used for Key Connector
|
||||
* purposes after authentication.
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* A token provided by the server as an authentication factor for sending
|
||||
* email OTPs to the user's configured 2FA email address. This is required
|
||||
* as we don't have a master password hash or other verifiable secret when using SSO.
|
||||
*/
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<SsoLoginStrategyData>): SsoLoginStrategyData {
|
||||
return Object.assign(new SsoLoginStrategyData(), obj, {
|
||||
tokenRequest: SsoTokenRequest.fromJSON(obj.tokenRequest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SsoLoginStrategy extends LoginStrategy {
|
||||
tokenRequest: SsoTokenRequest;
|
||||
orgId: string;
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.email}
|
||||
*/
|
||||
email$: Observable<string | null>;
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.orgId}
|
||||
*/
|
||||
orgId$: Observable<string>;
|
||||
/**
|
||||
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
|
||||
*/
|
||||
ssoEmail2FaSessionToken$: Observable<string | null>;
|
||||
|
||||
// A session token server side to serve as an authentication factor for the user
|
||||
// in order to send email OTPs to the user's configured 2FA email address
|
||||
// as we don't have a master password hash or other verifiable secret when using SSO.
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
email?: string; // email not preserved through SSO process so get from server
|
||||
protected cache: BehaviorSubject<SsoLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: SsoLoginStrategyData,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
@ -58,11 +95,17 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((state) => state.email));
|
||||
this.orgId$ = this.cache.pipe(map((state) => state.orgId));
|
||||
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
||||
}
|
||||
|
||||
async logIn(credentials: SsoLoginCredentials) {
|
||||
this.orgId = credentials.orgId;
|
||||
this.tokenRequest = new SsoTokenRequest(
|
||||
const data = new SsoLoginStrategyData();
|
||||
data.orgId = credentials.orgId;
|
||||
data.tokenRequest = new SsoTokenRequest(
|
||||
credentials.code,
|
||||
credentials.codeVerifier,
|
||||
credentials.redirectUrl,
|
||||
@ -70,16 +113,24 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
|
||||
this.cache.next(data);
|
||||
|
||||
const [ssoAuthResult] = await this.startLogIn();
|
||||
|
||||
this.email = ssoAuthResult.email;
|
||||
this.ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
||||
const email = ssoAuthResult.email;
|
||||
const ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
||||
|
||||
// Auth guard currently handles redirects for this.
|
||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||
await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset);
|
||||
}
|
||||
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
email,
|
||||
ssoEmail2FaSessionToken,
|
||||
});
|
||||
|
||||
return ssoAuthResult;
|
||||
}
|
||||
|
||||
@ -92,7 +143,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
// The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector.
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
if (newSsoUser) {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||
tokenResponse,
|
||||
this.cache.value.orgId,
|
||||
);
|
||||
} else {
|
||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||
@ -272,4 +326,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
sso: this.cache.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,11 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { UserApiLoginStrategy } from "./user-api-login.strategy";
|
||||
import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login.strategy";
|
||||
|
||||
describe("UserApiLoginStrategy", () => {
|
||||
let cache: UserApiLoginStrategyData;
|
||||
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
@ -60,6 +62,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
apiLogInStrategy = new UserApiLoginStrategy(
|
||||
cache,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
@ -13,13 +16,26 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { UserApiLoginCredentials } 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 UserApiLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: UserApiTokenRequest;
|
||||
captchaBypassToken: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<UserApiLoginStrategyData>): UserApiLoginStrategyData {
|
||||
return Object.assign(new UserApiLoginStrategyData(), obj, {
|
||||
tokenRequest: UserApiTokenRequest.fromJSON(obj.tokenRequest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class UserApiLoginStrategy extends LoginStrategy {
|
||||
tokenRequest: UserApiTokenRequest;
|
||||
protected cache: BehaviorSubject<UserApiLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: UserApiLoginStrategyData,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
@ -43,15 +59,18 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
);
|
||||
this.cache = new BehaviorSubject(data);
|
||||
}
|
||||
|
||||
override async logIn(credentials: UserApiLoginCredentials) {
|
||||
this.tokenRequest = new UserApiTokenRequest(
|
||||
const data = new UserApiLoginStrategyData();
|
||||
data.tokenRequest = new UserApiTokenRequest(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
await this.buildTwoFactor(),
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
this.cache.next(data);
|
||||
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
@ -84,7 +103,15 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
await super.saveAccountInformation(tokenResponse);
|
||||
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
|
||||
await this.stateService.setApiKeyClientSecret(this.tokenRequest.clientSecret);
|
||||
|
||||
const tokenRequest = this.cache.value.tokenRequest;
|
||||
await this.stateService.setApiKeyClientId(tokenRequest.clientId);
|
||||
await this.stateService.setApiKeyClientSecret(tokenRequest.clientSecret);
|
||||
}
|
||||
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
userApiKey: this.cache.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,11 @@ import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { WebAuthnLoginStrategy } from "./webauthn-login.strategy";
|
||||
import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-login.strategy";
|
||||
|
||||
describe("WebAuthnLoginStrategy", () => {
|
||||
let cache: WebAuthnLoginStrategyData;
|
||||
|
||||
let cryptoService!: MockProxy<CryptoService>;
|
||||
let apiService!: MockProxy<ApiService>;
|
||||
let tokenService!: MockProxy<TokenService>;
|
||||
@ -72,6 +74,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
webAuthnLoginStrategy = new WebAuthnLoginStrategy(
|
||||
cache,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
@ -286,7 +289,7 @@ function randomBytes(length: number): Uint8Array {
|
||||
// AuthenticatorAssertionResponse && PublicKeyCredential are only available in secure contexts
|
||||
// so we need to mock them and assign them to the global object to make them available
|
||||
// for the tests
|
||||
class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse {
|
||||
export class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse {
|
||||
clientDataJSON: ArrayBuffer = randomBytes(32).buffer;
|
||||
authenticatorData: ArrayBuffer = randomBytes(196).buffer;
|
||||
signature: ArrayBuffer = randomBytes(72).buffer;
|
||||
@ -298,7 +301,7 @@ class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionRespon
|
||||
userHandleB64Str = Utils.fromBufferToUrlB64(this.userHandle);
|
||||
}
|
||||
|
||||
class MockPublicKeyCredential implements PublicKeyCredential {
|
||||
export class MockPublicKeyCredential implements PublicKeyCredential {
|
||||
authenticatorAttachment = "cross-platform";
|
||||
id = "mockCredentialId";
|
||||
type = "public-key";
|
||||
|
@ -1,16 +1,86 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
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 } from "./login.strategy";
|
||||
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||
|
||||
export class WebAuthnLoginStrategyData implements LoginStrategyData {
|
||||
tokenRequest: WebAuthnLoginTokenRequest;
|
||||
captchaBypassToken?: string;
|
||||
credentials: WebAuthnLoginCredentials;
|
||||
|
||||
static fromJSON(obj: Jsonify<WebAuthnLoginStrategyData>): WebAuthnLoginStrategyData {
|
||||
return Object.assign(new WebAuthnLoginStrategyData(), obj, {
|
||||
tokenRequest: WebAuthnLoginTokenRequest.fromJSON(obj.tokenRequest),
|
||||
credentials: WebAuthnLoginCredentials.fromJSON(obj.credentials),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
tokenRequest: WebAuthnLoginTokenRequest;
|
||||
private credentials: WebAuthnLoginCredentials;
|
||||
protected cache: BehaviorSubject<WebAuthnLoginStrategyData>;
|
||||
|
||||
constructor(
|
||||
data: WebAuthnLoginStrategyData,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
}
|
||||
|
||||
async logIn(credentials: WebAuthnLoginCredentials) {
|
||||
const data = new WebAuthnLoginStrategyData();
|
||||
data.credentials = credentials;
|
||||
data.tokenRequest = new WebAuthnLoginTokenRequest(
|
||||
credentials.token,
|
||||
credentials.deviceResponse,
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
this.cache.next(data);
|
||||
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
|
||||
async logInTwoFactor(): Promise<AuthResult> {
|
||||
throw new Error("2FA not supported yet for WebAuthn Login.");
|
||||
}
|
||||
|
||||
protected override async setMasterKey() {
|
||||
return Promise.resolve();
|
||||
@ -29,15 +99,16 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
if (userDecryptionOptions?.webAuthnPrfOption) {
|
||||
const webAuthnPrfOption = idTokenResponse.userDecryptionOptions?.webAuthnPrfOption;
|
||||
|
||||
const credentials = this.cache.value.credentials;
|
||||
// confirm we still have the prf key
|
||||
if (!this.credentials.prfKey) {
|
||||
if (!credentials.prfKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// decrypt prf encrypted private key
|
||||
const privateKey = await this.cryptoService.decryptToBytes(
|
||||
webAuthnPrfOption.encryptedPrivateKey,
|
||||
this.credentials.prfKey,
|
||||
credentials.prfKey,
|
||||
);
|
||||
|
||||
// decrypt user key with private key
|
||||
@ -58,22 +129,9 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async logInTwoFactor(): Promise<AuthResult> {
|
||||
throw new Error("2FA not supported yet for WebAuthn Login.");
|
||||
}
|
||||
|
||||
async logIn(credentials: WebAuthnLoginCredentials) {
|
||||
// NOTE: To avoid DeadObject references on Firefox, do not set the credentials object directly
|
||||
// Use deep copy in future if objects are added that were created in popup
|
||||
this.credentials = { ...credentials };
|
||||
|
||||
this.tokenRequest = new WebAuthnLoginTokenRequest(
|
||||
credentials.token,
|
||||
credentials.deviceResponse,
|
||||
await this.buildDeviceRequest(),
|
||||
);
|
||||
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
exportCache(): CacheData {
|
||||
return {
|
||||
webAuthn: this.cache.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
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 { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
@ -28,7 +30,7 @@ export class SsoLoginCredentials {
|
||||
}
|
||||
|
||||
export class UserApiLoginCredentials {
|
||||
readonly type = AuthenticationType.UserApi;
|
||||
readonly type = AuthenticationType.UserApiKey;
|
||||
|
||||
constructor(
|
||||
public clientId: string,
|
||||
@ -48,6 +50,30 @@ export class AuthRequestLoginCredentials {
|
||||
public decryptedMasterKeyHash: string,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
) {}
|
||||
|
||||
static fromJSON(json: Jsonify<AuthRequestLoginCredentials>) {
|
||||
return Object.assign(
|
||||
new AuthRequestLoginCredentials(
|
||||
json.email,
|
||||
json.accessCode,
|
||||
json.authRequestId,
|
||||
null,
|
||||
null,
|
||||
json.decryptedMasterKeyHash,
|
||||
json.twoFactor
|
||||
? new TokenTwoFactorRequest(
|
||||
json.twoFactor.provider,
|
||||
json.twoFactor.token,
|
||||
json.twoFactor.remember,
|
||||
)
|
||||
: json.twoFactor,
|
||||
),
|
||||
{
|
||||
decryptedUserKey: SymmetricCryptoKey.fromJSON(json.decryptedUserKey) as UserKey,
|
||||
decryptedMasterKey: SymmetricCryptoKey.fromJSON(json.decryptedMasterKey) as MasterKey,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class WebAuthnLoginCredentials {
|
||||
@ -58,4 +84,15 @@ export class WebAuthnLoginCredentials {
|
||||
public deviceResponse: WebAuthnLoginAssertionResponseRequest,
|
||||
public prfKey?: SymmetricCryptoKey,
|
||||
) {}
|
||||
|
||||
static fromJSON(json: Jsonify<WebAuthnLoginCredentials>) {
|
||||
return new WebAuthnLoginCredentials(
|
||||
json.token,
|
||||
Object.assign(
|
||||
Object.create(WebAuthnLoginAssertionResponseRequest.prototype),
|
||||
json.deviceResponse,
|
||||
),
|
||||
SymmetricCryptoKey.fromJSON(json.prfKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,201 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-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 { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../../abstractions";
|
||||
import { PasswordLoginCredentials } from "../../models";
|
||||
|
||||
import { LoginStrategyService } from "./login-strategy.service";
|
||||
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
|
||||
|
||||
describe("LoginStrategyService", () => {
|
||||
let sut: LoginStrategyService;
|
||||
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
|
||||
let stateProvider: FakeGlobalStateProvider;
|
||||
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
i18nService = mock<I18nService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||
policyService = mock<PolicyService>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
stateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
sut = new LoginStrategyService(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
keyConnectorService,
|
||||
environmentService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
i18nService,
|
||||
encryptService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
deviceTrustCryptoService,
|
||||
authRequestService,
|
||||
stateProvider,
|
||||
);
|
||||
|
||||
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
|
||||
});
|
||||
|
||||
it("should return an AuthResult on successful login", async () => {
|
||||
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
|
||||
apiService.postIdentityToken.mockResolvedValue(
|
||||
new IdentityTokenResponse({
|
||||
ForcePasswordReset: false,
|
||||
Kdf: KdfType.Argon2id,
|
||||
Key: "KEY",
|
||||
PrivateKey: "PRIVATE_KEY",
|
||||
ResetMasterPassword: false,
|
||||
access_token: "ACCESS_TOKEN",
|
||||
expires_in: 3600,
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
scope: "api offline_access",
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
);
|
||||
tokenService.decodeToken.calledWith("ACCESS_TOKEN").mockResolvedValue({
|
||||
sub: "USER_ID",
|
||||
name: "NAME",
|
||||
email: "EMAIL",
|
||||
premium: false,
|
||||
});
|
||||
|
||||
const result = await sut.logIn(credentials);
|
||||
|
||||
expect(result).toBeInstanceOf(AuthResult);
|
||||
});
|
||||
|
||||
it("should return an AuthResult on successful 2fa login", async () => {
|
||||
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(
|
||||
new IdentityTwoFactorResponse({
|
||||
TwoFactorProviders: ["0"],
|
||||
TwoFactorProviders2: { 0: null },
|
||||
error: "invalid_grant",
|
||||
error_description: "Two factor required.",
|
||||
email: undefined,
|
||||
ssoEmail2faSessionToken: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
await sut.logIn(credentials);
|
||||
|
||||
const twoFactorToken = new TokenTwoFactorRequest(
|
||||
TwoFactorProviderType.Authenticator,
|
||||
"TWO_FACTOR_TOKEN",
|
||||
true,
|
||||
);
|
||||
apiService.postIdentityToken.mockResolvedValue(
|
||||
new IdentityTokenResponse({
|
||||
ForcePasswordReset: false,
|
||||
Kdf: KdfType.Argon2id,
|
||||
Key: "KEY",
|
||||
PrivateKey: "PRIVATE_KEY",
|
||||
ResetMasterPassword: false,
|
||||
access_token: "ACCESS_TOKEN",
|
||||
expires_in: 3600,
|
||||
refresh_token: "REFRESH_TOKEN",
|
||||
scope: "api offline_access",
|
||||
token_type: "Bearer",
|
||||
}),
|
||||
);
|
||||
|
||||
tokenService.decodeToken.calledWith("ACCESS_TOKEN").mockResolvedValue({
|
||||
sub: "USER_ID",
|
||||
name: "NAME",
|
||||
email: "EMAIL",
|
||||
premium: false,
|
||||
});
|
||||
|
||||
const result = await sut.logInTwoFactor(twoFactorToken, "CAPTCHA");
|
||||
|
||||
expect(result).toBeInstanceOf(AuthResult);
|
||||
});
|
||||
|
||||
it("should clear the cache if more than 2 mins have passed since expiration date", async () => {
|
||||
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
|
||||
apiService.postIdentityToken.mockResolvedValue(
|
||||
new IdentityTwoFactorResponse({
|
||||
TwoFactorProviders: ["0"],
|
||||
TwoFactorProviders2: { 0: null },
|
||||
error: "invalid_grant",
|
||||
error_description: "Two factor required.",
|
||||
email: undefined,
|
||||
ssoEmail2faSessionToken: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
await sut.logIn(credentials);
|
||||
|
||||
loginStrategyCacheExpirationState.stateSubject.next(new Date(Date.now() - 1000 * 60 * 5));
|
||||
|
||||
const twoFactorToken = new TokenTwoFactorRequest(
|
||||
TwoFactorProviderType.Authenticator,
|
||||
"TWO_FACTOR_TOKEN",
|
||||
true,
|
||||
);
|
||||
|
||||
await expect(sut.logInTwoFactor(twoFactorToken, "CAPTCHA")).rejects.toThrow();
|
||||
});
|
||||
});
|
@ -1,4 +1,12 @@
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import {
|
||||
combineLatestWith,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@ -10,6 +18,8 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
@ -23,6 +33,8 @@ 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 { KdfType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
@ -40,54 +52,35 @@ import {
|
||||
WebAuthnLoginCredentials,
|
||||
} from "../../models";
|
||||
|
||||
import {
|
||||
AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
|
||||
CURRENT_LOGIN_STRATEGY_KEY,
|
||||
CacheData,
|
||||
CACHE_EXPIRATION_KEY,
|
||||
CACHE_KEY,
|
||||
} from "./login-strategy.state";
|
||||
|
||||
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
get email(): string {
|
||||
if (
|
||||
this.logInStrategy instanceof PasswordLoginStrategy ||
|
||||
this.logInStrategy instanceof AuthRequestLoginStrategy ||
|
||||
this.logInStrategy instanceof SsoLoginStrategy
|
||||
) {
|
||||
return this.logInStrategy.email;
|
||||
}
|
||||
private sessionTimeout: unknown;
|
||||
private currentAuthnTypeState: GlobalState<AuthenticationType | null>;
|
||||
private loginStrategyCacheState: GlobalState<CacheData | null>;
|
||||
private loginStrategyCacheExpirationState: GlobalState<Date | null>;
|
||||
private authRequestPushNotificationState: GlobalState<string>;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get masterPasswordHash(): string {
|
||||
return this.logInStrategy instanceof PasswordLoginStrategy
|
||||
? this.logInStrategy.masterPasswordHash
|
||||
: null;
|
||||
}
|
||||
|
||||
get accessCode(): string {
|
||||
return this.logInStrategy instanceof AuthRequestLoginStrategy
|
||||
? this.logInStrategy.accessCode
|
||||
: null;
|
||||
}
|
||||
|
||||
get authRequestId(): string {
|
||||
return this.logInStrategy instanceof AuthRequestLoginStrategy
|
||||
? this.logInStrategy.authRequestId
|
||||
: null;
|
||||
}
|
||||
|
||||
get ssoEmail2FaSessionToken(): string {
|
||||
return this.logInStrategy instanceof SsoLoginStrategy
|
||||
? this.logInStrategy.ssoEmail2FaSessionToken
|
||||
: null;
|
||||
}
|
||||
|
||||
private logInStrategy:
|
||||
private loginStrategy$: Observable<
|
||||
| UserApiLoginStrategy
|
||||
| PasswordLoginStrategy
|
||||
| SsoLoginStrategy
|
||||
| AuthRequestLoginStrategy
|
||||
| WebAuthnLoginStrategy;
|
||||
private sessionTimeout: any;
|
||||
| WebAuthnLoginStrategy
|
||||
| null
|
||||
>;
|
||||
|
||||
private pushNotificationSubject = new Subject<string>();
|
||||
currentAuthType$: Observable<AuthenticationType | null>;
|
||||
// TODO: move to auth request service
|
||||
authRequestPushNotification$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
protected cryptoService: CryptoService,
|
||||
@ -107,7 +100,71 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
protected policyService: PolicyService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
protected authRequestService: AuthRequestServiceAbstraction,
|
||||
) {}
|
||||
protected stateProvider: GlobalStateProvider,
|
||||
) {
|
||||
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
|
||||
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
|
||||
this.loginStrategyCacheExpirationState = this.stateProvider.get(CACHE_EXPIRATION_KEY);
|
||||
this.authRequestPushNotificationState = this.stateProvider.get(
|
||||
AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
|
||||
);
|
||||
|
||||
this.currentAuthType$ = this.currentAuthnTypeState.state$;
|
||||
this.authRequestPushNotification$ = this.authRequestPushNotificationState.state$.pipe(
|
||||
filter((id) => id != null),
|
||||
);
|
||||
this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe(
|
||||
distinctUntilChanged(),
|
||||
combineLatestWith(this.loginStrategyCacheState.state$),
|
||||
this.initializeLoginStrategy.bind(this),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
}
|
||||
|
||||
async getEmail(): Promise<string | null> {
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
if ("email$" in strategy) {
|
||||
return await firstValueFrom(strategy.email$);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getMasterPasswordHash(): Promise<string | null> {
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
if ("masterKeyHash$" in strategy) {
|
||||
return await firstValueFrom(strategy.masterKeyHash$);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getSsoEmail2FaSessionToken(): Promise<string | null> {
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
if ("ssoEmail2FaSessionToken$" in strategy) {
|
||||
return await firstValueFrom(strategy.ssoEmail2FaSessionToken$);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getAccessCode(): Promise<string | null> {
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
if ("accessCode$" in strategy) {
|
||||
return await firstValueFrom(strategy.accessCode$);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getAuthRequestId(): Promise<string | null> {
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
if ("authRequestId$" in strategy) {
|
||||
return await firstValueFrom(strategy.authRequestId$);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async logIn(
|
||||
credentials:
|
||||
@ -117,99 +174,27 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
| AuthRequestLoginCredentials
|
||||
| WebAuthnLoginCredentials,
|
||||
): Promise<AuthResult> {
|
||||
this.clearState();
|
||||
await this.clearCache();
|
||||
|
||||
let strategy:
|
||||
| UserApiLoginStrategy
|
||||
| PasswordLoginStrategy
|
||||
| SsoLoginStrategy
|
||||
| AuthRequestLoginStrategy
|
||||
| WebAuthnLoginStrategy;
|
||||
await this.currentAuthnTypeState.update((_) => credentials.type);
|
||||
|
||||
switch (credentials.type) {
|
||||
case AuthenticationType.Password:
|
||||
strategy = new PasswordLoginStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.Sso:
|
||||
strategy = new SsoLoginStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
this.i18nService,
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.UserApi:
|
||||
strategy = new UserApiLoginStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.environmentService,
|
||||
this.keyConnectorService,
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.AuthRequest:
|
||||
strategy = new AuthRequestLoginStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.deviceTrustCryptoService,
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.WebAuthn:
|
||||
strategy = new WebAuthnLoginStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
);
|
||||
break;
|
||||
}
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
|
||||
// Note: Do not set the credentials object directly on the strategy. They are
|
||||
// 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.
|
||||
const result = await strategy.logIn(credentials as any);
|
||||
// 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 };
|
||||
|
||||
if (result?.requiresTwoFactor) {
|
||||
this.saveState(strategy);
|
||||
const result = await strategy.logIn(ownedCredentials as any);
|
||||
|
||||
if (result != null && !result.requiresTwoFactor) {
|
||||
await this.clearCache();
|
||||
} else {
|
||||
// Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts
|
||||
await this.loginStrategyCacheState.update((_) => strategy.exportCache());
|
||||
await this.startSessionTimeout();
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -219,43 +204,32 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string,
|
||||
): Promise<AuthResult> {
|
||||
if (this.logInStrategy == null) {
|
||||
if (!(await this.isSessionValid())) {
|
||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
|
||||
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||
if (strategy == null) {
|
||||
throw new Error("No login strategy found.");
|
||||
}
|
||||
|
||||
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
|
||||
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
|
||||
this.clearState();
|
||||
try {
|
||||
const result = await strategy.logInTwoFactor(twoFactor, captchaResponse);
|
||||
|
||||
// Only clear cache if 2FA token has been accepted, otherwise we need to be able to try again
|
||||
if (result != null && !result.requiresTwoFactor && !result.requiresCaptcha) {
|
||||
await this.clearCache();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
|
||||
// API exceptions are okay, but if there are any unhandled client-side errors then clear cache to be safe
|
||||
if (!(e instanceof ErrorResponse)) {
|
||||
this.clearState();
|
||||
await this.clearCache();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
authingWithUserApiKey(): boolean {
|
||||
return this.logInStrategy instanceof UserApiLoginStrategy;
|
||||
}
|
||||
|
||||
authingWithSso(): boolean {
|
||||
return this.logInStrategy instanceof SsoLoginStrategy;
|
||||
}
|
||||
|
||||
authingWithPassword(): boolean {
|
||||
return this.logInStrategy instanceof PasswordLoginStrategy;
|
||||
}
|
||||
|
||||
authingWithPasswordless(): boolean {
|
||||
return this.logInStrategy instanceof AuthRequestLoginStrategy;
|
||||
}
|
||||
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
|
||||
email = email.trim().toLowerCase();
|
||||
let kdf: KdfType = null;
|
||||
@ -278,39 +252,171 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
|
||||
}
|
||||
|
||||
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> {
|
||||
this.pushNotificationSubject.next(notification.id);
|
||||
}
|
||||
|
||||
getPushNotificationObs$(): Observable<any> {
|
||||
return this.pushNotificationSubject.asObservable();
|
||||
}
|
||||
|
||||
private saveState(
|
||||
strategy:
|
||||
| UserApiLoginStrategy
|
||||
| PasswordLoginStrategy
|
||||
| SsoLoginStrategy
|
||||
| AuthRequestLoginStrategy
|
||||
| WebAuthnLoginStrategy,
|
||||
) {
|
||||
this.logInStrategy = strategy;
|
||||
this.startSessionTimeout();
|
||||
}
|
||||
|
||||
private clearState() {
|
||||
this.logInStrategy = null;
|
||||
this.clearSessionTimeout();
|
||||
}
|
||||
|
||||
private startSessionTimeout() {
|
||||
this.clearSessionTimeout();
|
||||
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
|
||||
}
|
||||
|
||||
private clearSessionTimeout() {
|
||||
if (this.sessionTimeout != null) {
|
||||
clearTimeout(this.sessionTimeout);
|
||||
// TODO move to auth request service
|
||||
async sendAuthRequestPushNotification(notification: AuthRequestPushNotification): Promise<void> {
|
||||
if (notification.id != null) {
|
||||
await this.authRequestPushNotificationState.update((_) => notification.id);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to auth request service
|
||||
async passwordlessLogin(
|
||||
id: string,
|
||||
key: string,
|
||||
requestApproved: boolean,
|
||||
): Promise<AuthRequestResponse> {
|
||||
const pubKey = Utils.fromB64ToArray(key);
|
||||
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
let keyToEncrypt;
|
||||
let encryptedMasterKeyHash = null;
|
||||
|
||||
if (masterKey) {
|
||||
keyToEncrypt = masterKey.encKey;
|
||||
|
||||
// Only encrypt the master password hash if masterKey exists as
|
||||
// we won't have a masterKeyHash without a masterKey
|
||||
const masterKeyHash = await this.stateService.getKeyHash();
|
||||
if (masterKeyHash != null) {
|
||||
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(masterKeyHash),
|
||||
pubKey,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
keyToEncrypt = userKey.key;
|
||||
}
|
||||
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
|
||||
|
||||
const request = new PasswordlessAuthRequest(
|
||||
encryptedKey.encryptedString,
|
||||
encryptedMasterKeyHash?.encryptedString,
|
||||
await this.appIdService.getAppId(),
|
||||
requestApproved,
|
||||
);
|
||||
return await this.apiService.putAuthRequest(id, request);
|
||||
}
|
||||
|
||||
private async clearCache(): Promise<void> {
|
||||
await this.currentAuthnTypeState.update((_) => null);
|
||||
await this.loginStrategyCacheState.update((_) => null);
|
||||
await this.clearSessionTimeout();
|
||||
}
|
||||
|
||||
private async startSessionTimeout(): Promise<void> {
|
||||
await this.clearSessionTimeout();
|
||||
await this.loginStrategyCacheExpirationState.update(
|
||||
(_) => new Date(Date.now() + sessionTimeoutLength),
|
||||
);
|
||||
this.sessionTimeout = setTimeout(() => this.clearCache(), sessionTimeoutLength);
|
||||
}
|
||||
|
||||
private async clearSessionTimeout(): Promise<void> {
|
||||
await this.loginStrategyCacheExpirationState.update((_) => null);
|
||||
this.sessionTimeout = null;
|
||||
}
|
||||
|
||||
private async isSessionValid(): Promise<boolean> {
|
||||
const cache = await firstValueFrom(this.loginStrategyCacheState.state$);
|
||||
if (cache == null) {
|
||||
return false;
|
||||
}
|
||||
const expiration = await firstValueFrom(this.loginStrategyCacheExpirationState.state$);
|
||||
if (expiration != null && expiration < new Date()) {
|
||||
await this.clearCache();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private initializeLoginStrategy(
|
||||
source: Observable<[AuthenticationType | null, CacheData | null]>,
|
||||
) {
|
||||
return source.pipe(
|
||||
map(([strategy, data]) => {
|
||||
if (strategy == null) {
|
||||
return null;
|
||||
}
|
||||
switch (strategy) {
|
||||
case AuthenticationType.Password:
|
||||
return new PasswordLoginStrategy(
|
||||
data?.password,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
);
|
||||
case AuthenticationType.Sso:
|
||||
return new SsoLoginStrategy(
|
||||
data?.sso,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authRequestService,
|
||||
this.i18nService,
|
||||
);
|
||||
case AuthenticationType.UserApiKey:
|
||||
return new UserApiLoginStrategy(
|
||||
data?.userApiKey,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.environmentService,
|
||||
this.keyConnectorService,
|
||||
);
|
||||
case AuthenticationType.AuthRequest:
|
||||
return new AuthRequestLoginStrategy(
|
||||
data?.authRequest,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.deviceTrustCryptoService,
|
||||
);
|
||||
case AuthenticationType.WebAuthn:
|
||||
return new WebAuthnLoginStrategy(
|
||||
data?.webAuthn,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,155 @@
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.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";
|
||||
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey, PrfKey, UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-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";
|
||||
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
|
||||
import {
|
||||
MockAuthenticatorAssertionResponse,
|
||||
MockPublicKeyCredential,
|
||||
} from "../../login-strategies/webauthn-login.strategy.spec";
|
||||
import { AuthRequestLoginCredentials, WebAuthnLoginCredentials } from "../../models";
|
||||
|
||||
import { CACHE_KEY } from "./login-strategy.state";
|
||||
|
||||
describe("LOGIN_STRATEGY_CACHE_KEY", () => {
|
||||
const sut = CACHE_KEY;
|
||||
|
||||
let deviceRequest: DeviceRequest;
|
||||
let twoFactorRequest: TokenTwoFactorRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
deviceRequest = Object.assign(Object.create(DeviceRequest.prototype), {
|
||||
type: DeviceType.ChromeBrowser,
|
||||
name: "DEVICE_NAME",
|
||||
identifier: "DEVICE_IDENTIFIER",
|
||||
pushToken: "PUSH_TOKEN",
|
||||
});
|
||||
|
||||
twoFactorRequest = new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false);
|
||||
});
|
||||
|
||||
it("should correctly deserialize PasswordLoginStrategyData", () => {
|
||||
const actual = {
|
||||
password: new PasswordLoginStrategyData(),
|
||||
};
|
||||
actual.password.tokenRequest = new PasswordTokenRequest(
|
||||
"EMAIL",
|
||||
"LOCAL_PASSWORD_HASH",
|
||||
"CAPTCHA_TOKEN",
|
||||
twoFactorRequest,
|
||||
deviceRequest,
|
||||
);
|
||||
actual.password.masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey;
|
||||
actual.password.localMasterKeyHash = "LOCAL_MASTER_KEY_HASH";
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||
|
||||
expect(result.password).toBeInstanceOf(PasswordLoginStrategyData);
|
||||
verifyPropertyPrototypes(result, actual);
|
||||
});
|
||||
|
||||
it("should correctly deserialize SsoLoginStrategyData", () => {
|
||||
const actual = { sso: new SsoLoginStrategyData() };
|
||||
actual.sso.tokenRequest = new SsoTokenRequest(
|
||||
"CODE",
|
||||
"CODE_VERIFIER",
|
||||
"REDIRECT_URI",
|
||||
twoFactorRequest,
|
||||
deviceRequest,
|
||||
);
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||
|
||||
expect(result.sso).toBeInstanceOf(SsoLoginStrategyData);
|
||||
verifyPropertyPrototypes(result, actual);
|
||||
});
|
||||
|
||||
it("should correctly deserialize UserApiLoginStrategyData", () => {
|
||||
const actual = { userApiKey: new UserApiLoginStrategyData() };
|
||||
actual.userApiKey.tokenRequest = new UserApiTokenRequest("CLIENT_ID", "CLIENT_SECRET", null);
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||
|
||||
expect(result.userApiKey).toBeInstanceOf(UserApiLoginStrategyData);
|
||||
verifyPropertyPrototypes(result, actual);
|
||||
});
|
||||
|
||||
it("should correctly deserialize AuthRequestLoginStrategyData", () => {
|
||||
const actual = { authRequest: new AuthRequestLoginStrategyData() };
|
||||
actual.authRequest.tokenRequest = new PasswordTokenRequest("EMAIL", "ACCESS_CODE", null, null);
|
||||
actual.authRequest.authRequestCredentials = new AuthRequestLoginCredentials(
|
||||
"EMAIL",
|
||||
"ACCESS_CODE",
|
||||
"AUTH_REQUEST_ID",
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey,
|
||||
"MASTER_KEY_HASH",
|
||||
);
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||
|
||||
expect(result.authRequest).toBeInstanceOf(AuthRequestLoginStrategyData);
|
||||
verifyPropertyPrototypes(result, actual);
|
||||
});
|
||||
|
||||
it("should correctly deserialize WebAuthnLoginStrategyData", () => {
|
||||
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
|
||||
const actual = { webAuthn: new WebAuthnLoginStrategyData() };
|
||||
const publicKeyCredential = new MockPublicKeyCredential();
|
||||
const deviceResponse = new WebAuthnLoginAssertionResponseRequest(publicKeyCredential);
|
||||
const prfKey = new SymmetricCryptoKey(new Uint8Array(64)) as PrfKey;
|
||||
actual.webAuthn.credentials = new WebAuthnLoginCredentials("TOKEN", deviceResponse, prfKey);
|
||||
actual.webAuthn.tokenRequest = new WebAuthnLoginTokenRequest(
|
||||
"TOKEN",
|
||||
deviceResponse,
|
||||
deviceRequest,
|
||||
);
|
||||
actual.webAuthn.captchaBypassToken = "CAPTCHA_BYPASS_TOKEN";
|
||||
actual.webAuthn.tokenRequest.setTwoFactor(
|
||||
new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false),
|
||||
);
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||
|
||||
expect(result.webAuthn).toBeInstanceOf(WebAuthnLoginStrategyData);
|
||||
verifyPropertyPrototypes(result, actual);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Recursively verifies the prototypes of all objects in the deserialized object.
|
||||
* It is important that the concrete object has the correct prototypes for
|
||||
* comparison.
|
||||
* @param deserialized the deserialized object
|
||||
* @param concrete the object stored in state
|
||||
*/
|
||||
function verifyPropertyPrototypes(deserialized: object, concrete: object) {
|
||||
for (const key of Object.keys(deserialized)) {
|
||||
const deserializedProperty = (deserialized as any)[key];
|
||||
if (deserializedProperty === undefined) {
|
||||
continue;
|
||||
}
|
||||
const realProperty = (concrete as any)[key];
|
||||
if (realProperty === undefined) {
|
||||
throw new Error(`Expected ${key} to be defined in ${concrete.constructor.name}`);
|
||||
}
|
||||
// we only care about checking prototypes of objects
|
||||
if (typeof realProperty === "object" && realProperty !== null) {
|
||||
const realProto = Object.getPrototypeOf(realProperty);
|
||||
expect(deserializedProperty).toBeInstanceOf(realProto.constructor);
|
||||
verifyPropertyPrototypes(deserializedProperty, realProperty);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { KeyDefinition, LOGIN_STRATEGY_MEMORY } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-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";
|
||||
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
|
||||
|
||||
/**
|
||||
* The current login strategy in use.
|
||||
*/
|
||||
export const CURRENT_LOGIN_STRATEGY_KEY = new KeyDefinition<AuthenticationType | null>(
|
||||
LOGIN_STRATEGY_MEMORY,
|
||||
"currentLoginStrategy",
|
||||
{
|
||||
deserializer: (data) => data,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The expiration date for the login strategy cache.
|
||||
* Used as a backup to the timer set on the service.
|
||||
*/
|
||||
export const CACHE_EXPIRATION_KEY = new KeyDefinition<Date | null>(
|
||||
LOGIN_STRATEGY_MEMORY,
|
||||
"loginStrategyCacheExpiration",
|
||||
{
|
||||
deserializer: (data) => (data ? null : new Date(data)),
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Auth Request notification for all instances of the login strategy service.
|
||||
* Note: this isn't an ideal approach, but allows both a background and
|
||||
* foreground instance to send out the notification.
|
||||
* TODO: Move to Auth Request service.
|
||||
*/
|
||||
export const AUTH_REQUEST_PUSH_NOTIFICATION_KEY = new KeyDefinition<string>(
|
||||
LOGIN_STRATEGY_MEMORY,
|
||||
"authRequestPushNotification",
|
||||
{
|
||||
deserializer: (data) => data,
|
||||
},
|
||||
);
|
||||
|
||||
export type CacheData = {
|
||||
password?: PasswordLoginStrategyData;
|
||||
sso?: SsoLoginStrategyData;
|
||||
userApiKey?: UserApiLoginStrategyData;
|
||||
authRequest?: AuthRequestLoginStrategyData;
|
||||
webAuthn?: WebAuthnLoginStrategyData;
|
||||
};
|
||||
|
||||
/**
|
||||
* A cache for login strategies to use for data persistence through
|
||||
* the login process.
|
||||
*/
|
||||
export const CACHE_KEY = new KeyDefinition<CacheData | null>(
|
||||
LOGIN_STRATEGY_MEMORY,
|
||||
"loginStrategyCache",
|
||||
{
|
||||
deserializer: (data) => {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
password: data.password ? PasswordLoginStrategyData.fromJSON(data.password) : undefined,
|
||||
sso: data.sso ? SsoLoginStrategyData.fromJSON(data.sso) : undefined,
|
||||
userApiKey: data.userApiKey
|
||||
? UserApiLoginStrategyData.fromJSON(data.userApiKey)
|
||||
: undefined,
|
||||
authRequest: data.authRequest
|
||||
? AuthRequestLoginStrategyData.fromJSON(data.authRequest)
|
||||
: undefined,
|
||||
webAuthn: data.webAuthn ? WebAuthnLoginStrategyData.fromJSON(data.webAuthn) : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
@ -1,7 +1,7 @@
|
||||
export enum AuthenticationType {
|
||||
Password = 0,
|
||||
Sso = 1,
|
||||
UserApi = 2,
|
||||
UserApiKey = 2,
|
||||
AuthRequest = 3,
|
||||
WebAuthn = 4,
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeviceType } from "../../../../enums";
|
||||
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
|
||||
|
||||
@ -13,4 +15,8 @@ export class DeviceRequest {
|
||||
this.identifier = appId;
|
||||
this.pushToken = null;
|
||||
}
|
||||
|
||||
static fromJSON(json: Jsonify<DeviceRequest>) {
|
||||
return Object.assign(Object.create(DeviceRequest.prototype), json);
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,13 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
|
||||
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||
twoFactor: json.twoFactor
|
||||
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -23,4 +23,13 @@ export class SsoTokenRequest extends TokenRequest {
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
return Object.assign(Object.create(SsoTokenRequest.prototype), json, {
|
||||
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||
twoFactor: json.twoFactor
|
||||
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -21,4 +21,13 @@ export class UserApiTokenRequest extends TokenRequest {
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
return Object.assign(Object.create(UserApiTokenRequest.prototype), json, {
|
||||
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||
twoFactor: json.twoFactor
|
||||
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
|
||||
import { DeviceRequest } from "./device.request";
|
||||
import { TokenTwoFactorRequest } from "./token-two-factor.request";
|
||||
import { TokenRequest } from "./token.request";
|
||||
|
||||
export class WebAuthnLoginTokenRequest extends TokenRequest {
|
||||
@ -22,4 +23,14 @@ export class WebAuthnLoginTokenRequest extends TokenRequest {
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
return Object.assign(Object.create(WebAuthnLoginTokenRequest.prototype), json, {
|
||||
deviceResponse: WebAuthnLoginAssertionResponseRequest.fromJSON(json.deviceResponse),
|
||||
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||
twoFactor: json.twoFactor
|
||||
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction {
|
||||
}
|
||||
|
||||
private async ProcessNotification(notification: NotificationResponse) {
|
||||
await this.loginStrategyService.authResponsePushNotification(
|
||||
await this.loginStrategyService.sendAuthRequestPushNotification(
|
||||
notification.payload as AuthRequestPushNotification,
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
|
||||
import { WebAuthnLoginResponseRequest } from "./webauthn-login-response.request";
|
||||
@ -27,4 +29,8 @@ export class WebAuthnLoginAssertionResponseRequest extends WebAuthnLoginResponse
|
||||
userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle),
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(json: Jsonify<WebAuthnLoginAssertionResponseRequest>) {
|
||||
return Object.assign(Object.create(WebAuthnLoginAssertionResponseRequest.prototype), json);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
||||
|
||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
|
||||
// Autofill
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user