1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00
Files
browser/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts

827 lines
31 KiB
TypeScript

import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { IsActiveMatchOptions, Router, RouterModule } from "@angular/router";
import { Observable, firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
AuthRequestLoginCredentials,
AuthRequestServiceAbstraction,
LoginEmailServiceAbstraction,
LoginStrategyServiceAbstraction,
LoginSuccessHandlerService,
} from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { AuthRequestApiServiceAbstraction } from "../../common/abstractions/auth-request-api.service";
import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum Flow {
StandardAuthRequest, // when user clicks "Login with device" from /login or "Approve from your other device" from /login-initiated
AdminAuthRequest, // when user clicks "Request admin approval" from /login-initiated
}
const matchOptions: IsActiveMatchOptions = {
paths: "exact",
queryParams: "ignored",
fragment: "ignored",
matrixParams: "ignored",
};
@Component({
templateUrl: "./login-via-auth-request.component.html",
imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule],
providers: [{ provide: LoginViaAuthRequestCacheService }],
})
export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private authRequestKeyPair:
| { publicKey: Uint8Array | undefined; privateKey: Uint8Array | undefined }
| undefined = undefined;
private accessCode: string | undefined = undefined;
private authStatus: AuthenticationStatus | undefined = undefined;
private showResendNotificationTimeoutSeconds = 12;
protected loading = true;
protected backToRoute = "/login";
protected clientType: ClientType;
protected ClientType = ClientType;
protected email: string | undefined = undefined;
protected fingerprintPhrase: string | undefined = undefined;
protected showResendNotification = false;
protected Flow = Flow;
protected flow = Flow.StandardAuthRequest;
protected webVaultUrl: string | undefined = undefined;
protected deviceManagementUrl: string | undefined;
constructor(
private accountService: AccountService,
private anonymousHubService: AnonymousHubService,
private appIdService: AppIdService,
private authRequestApiService: AuthRequestApiServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private authService: AuthService,
private cryptoFunctionService: CryptoFunctionService,
private deviceTrustService: DeviceTrustServiceAbstraction,
private environmentService: EnvironmentService,
private i18nService: I18nService,
private logService: LogService,
private loginEmailService: LoginEmailServiceAbstraction,
private loginStrategyService: LoginStrategyServiceAbstraction,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private router: Router,
private toastService: ToastService,
private validationService: ValidationService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private loginViaAuthRequestCacheService: LoginViaAuthRequestCacheService,
) {
this.clientType = this.platformUtilsService.getClientType();
// Gets SignalR push notification
// Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$
.pipe(takeUntilDestroyed())
.subscribe((requestId) => {
this.loading = true;
this.handleExistingAuthRequestLogin(requestId).catch((e: Error) => {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
});
this.loading = false;
this.logService.error("Failed to use approved auth request: " + e.message);
});
});
// Get the web vault URL from the environment service
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.webVaultUrl = env.getWebVaultUrl();
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
});
}
async ngOnInit(): Promise<void> {
// Get the authStatus early because we use it in both flows
this.authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
if (userHasAuthenticatedViaSSO) {
this.backToRoute = "/login-initiated";
}
/**
* The LoginViaAuthRequestComponent handles both the `login-with-device` and
* the `admin-approval-requested` routes. Therefore, we check the route to determine
* which flow to initialize.
*/
if (this.router.isActive("admin-approval-requested", matchOptions)) {
await this.initAdminAuthRequestFlow();
} else {
await this.initStandardAuthRequestFlow();
}
this.loading = false;
}
private async initAdminAuthRequestFlow(): Promise<void> {
this.flow = Flow.AdminAuthRequest;
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (!userId) {
this.logService.error(
"Not able to get a user id from the account service active account observable.",
);
return;
}
// [Admin Request Flow State Management] Check cached auth request
const existingAdminAuthRequest = await this.reloadCachedAdminAuthRequest(userId);
if (existingAdminAuthRequest) {
await this.handleExistingAdminAuthRequestLogin(existingAdminAuthRequest, userId);
} else {
await this.handleNewAdminAuthRequestLogin();
}
}
private async initStandardAuthRequestFlow(): Promise<void> {
this.flow = Flow.StandardAuthRequest;
// For a standard flow, we can get the user's email from two different places:
// 1. The loginEmailService, which is the email that the user is trying to log in with. This is cleared
// when the user logs in successfully. We can use this when the user is using Login with Device.
// 2. With TDE Login with Another Device, the user is already logged in and we just need to get
// a decryption key, so we can use the active account's email.
const activeAccountEmail$: Observable<string | undefined> =
this.accountService.activeAccount$.pipe(map((a) => a?.email));
const loginEmail$: Observable<string | null> = this.loginEmailService.loginEmail$;
let loginEmail: string | undefined = (await firstValueFrom(loginEmail$)) ?? undefined;
if (!loginEmail) {
loginEmail = (await firstValueFrom(activeAccountEmail$)) ?? undefined;
}
// Wait for either the first email or the timeout to occur so we can proceed
// neither above observable will complete, so we have to add a timeout
this.email = loginEmail;
if (!this.email) {
await this.handleMissingEmail();
return;
}
// [Standard Flow State Management] Check cached auth request
const cachedAuthRequest: LoginViaAuthRequestView | null =
this.loginViaAuthRequestCacheService.getCachedLoginViaAuthRequestView();
if (cachedAuthRequest) {
this.logService.info("Found cached auth request.");
if (!cachedAuthRequest.id) {
this.logService.error(
"No id on the cached auth request when in the standard auth request flow.",
);
return;
}
await this.reloadCachedStandardAuthRequest(cachedAuthRequest);
await this.handleExistingAuthRequestLogin(cachedAuthRequest.id);
} else {
await this.handleNewStandardAuthRequestLogin();
}
}
private async handleMissingEmail(): Promise<void> {
this.toastService.showToast({
variant: "error",
message: this.i18nService.t("userEmailMissing"),
});
await this.router.navigate([this.backToRoute]);
}
async ngOnDestroy(): Promise<void> {
await this.anonymousHubService.stopHubConnection();
this.loginViaAuthRequestCacheService.clearCacheLoginView();
}
private async handleNewAdminAuthRequestLogin(): Promise<void> {
try {
if (!this.email) {
this.logService.error("No email when starting admin auth request login.");
return;
}
// At this point we know there is no
const authRequest = await this.buildAuthRequest(this.email, AuthRequestType.AdminApproval);
if (!authRequest) {
this.logService.error("Auth request failed to build.");
return;
}
if (!this.authRequestKeyPair) {
this.logService.error("Key pairs failed to initialize from buildAuthRequest.");
return;
}
const authRequestResponse =
await this.authRequestApiService.postAdminAuthRequest(authRequest);
const adminAuthReqStorable = new AdminAuthRequestStorable({
id: authRequestResponse.id,
privateKey: this.authRequestKeyPair.privateKey,
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (!userId) {
this.logService.error(
"Not able to get a user id from the account service active account observable.",
);
return;
}
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
if (authRequestResponse.id) {
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
}
} catch (e) {
this.logService.error(e);
}
}
/**
* We only allow a single admin approval request to be active at a time
* so we can check to see if it's stored in state with the state service
* provider.
* @param userId
* @protected
*/
protected async reloadCachedAdminAuthRequest(
userId: UserId,
): Promise<AdminAuthRequestStorable | null> {
// Get email from state for admin auth requests because it is available and also
// prevents it from being lost on refresh as the loginEmailService email does not persist.
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
if (!this.email) {
await this.handleMissingEmail();
return null;
}
return await this.authRequestService.getAdminAuthRequest(userId);
}
/**
* Restores a cached authentication request into the component's state.
*
* This function checks for the presence of a cached authentication request and,
* if available, updates the component's state with the necessary details to
* continue processing the request. It ensures that the user's email and the
* private key from the cached request are available.
*
* The private key is converted from Base64 to an ArrayBuffer, and a fingerprint
* phrase is derived to verify the request's integrity. The function then sets
* the authentication request key pair in the component's state, preparing it
* to handle any responses or approvals.
*
* @param cachedAuthRequest The request to load into the component state
* @returns Promise to await for completion
*/
protected async reloadCachedStandardAuthRequest(
cachedAuthRequest: LoginViaAuthRequestView,
): Promise<void> {
if (cachedAuthRequest) {
if (!this.email) {
this.logService.error(
"Email not defined when trying to reload cached standard auth request.",
);
return;
}
if (!cachedAuthRequest.privateKey) {
this.logService.error(
"No private key on the cached auth request when trying to reload cached standard auth request.",
);
return;
}
if (!cachedAuthRequest.accessCode) {
this.logService.error(
"No access code on the cached auth request when trying to reload cached standard auth request.",
);
return;
}
const privateKey = Utils.fromB64ToArray(cachedAuthRequest.privateKey);
// Re-derive the user's fingerprint phrase
// It is important to not use the server's public key here as it could have been compromised via MITM
const derivedPublicKeyArrayBuffer =
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
derivedPublicKeyArrayBuffer,
);
// We don't need the public key for handling the authentication request because
// the handleExistingAuthRequestLogin function will receive the public key back
// from the looked up auth request, and all we need is to make sure that
// we can use the cached private key that is associated with it.
this.authRequestKeyPair = {
privateKey: privateKey,
publicKey: undefined,
};
this.accessCode = cachedAuthRequest.accessCode;
}
}
protected async handleNewStandardAuthRequestLogin(): Promise<void> {
this.showResendNotification = false;
try {
if (!this.email) {
this.logService.error("Email not defined when starting standard auth request login.");
return;
}
const authRequest = await this.buildAuthRequest(
this.email,
AuthRequestType.AuthenticateAndUnlock,
);
// I tried several ways to get the IDE/linter to play nice with checking for null values
// in less code / more efficiently, but it struggles to identify code paths that
// are more complicated than this.
if (!authRequest) {
this.logService.error("AuthRequest failed to initialize from buildAuthRequest.");
return;
}
if (!this.fingerprintPhrase) {
this.logService.error("FingerprintPhrase failed to initialize from buildAuthRequest.");
return;
}
if (!this.authRequestKeyPair) {
this.logService.error("KeyPair failed to initialize from buildAuthRequest.");
return;
}
const authRequestResponse: AuthRequestResponse =
await this.authRequestApiService.postAuthRequest(authRequest);
if (!this.authRequestKeyPair.privateKey) {
this.logService.error("No private key when trying to cache the login view.");
return;
}
if (!this.accessCode) {
this.logService.error("No access code when trying to cache the login view.");
return;
}
this.loginViaAuthRequestCacheService.cacheLoginView(
authRequestResponse.id,
this.authRequestKeyPair.privateKey,
this.accessCode,
);
if (authRequestResponse.id) {
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
}
} catch (e) {
this.logService.error(e);
}
setTimeout(() => {
this.showResendNotification = true;
}, this.showResendNotificationTimeoutSeconds * 1000);
}
private async buildAuthRequest(
email: string,
authRequestType: AuthRequestType,
): Promise<AuthRequest> {
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
this.authRequestKeyPair = {
publicKey: authRequestKeyPairArray[0],
privateKey: authRequestKeyPairArray[1],
};
const deviceIdentifier = await this.appIdService.getAppId();
if (!this.authRequestKeyPair.publicKey) {
const errorMessage = "No public key when building an auth request.";
this.logService.error(errorMessage);
throw new Error(errorMessage);
}
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
email,
this.authRequestKeyPair.publicKey,
);
this.accessCode = await this.passwordGenerationService.generatePassword({
type: "password",
length: 25,
});
const b64PublicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
return new AuthRequest(email, deviceIdentifier, b64PublicKey, authRequestType, this.accessCode);
}
private async handleExistingAdminAuthRequestLogin(
adminAuthRequestStorable: AdminAuthRequestStorable,
userId: UserId,
): Promise<void> {
// Note: on login, the SSOLoginStrategy will also call to see if an existing admin auth req
// has been approved and handle it if so.
// Regardless, we always retrieve the auth request from the server and verify and handle status changes here as well
let adminAuthRequestResponse: AuthRequestResponse;
try {
adminAuthRequestResponse = await this.authRequestApiService.getAuthRequest(
adminAuthRequestStorable.id,
);
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
return await this.clearExistingAdminAuthRequestAndStartNewRequest(userId);
}
this.logService.error(error);
return;
}
// Request doesn't exist anymore
if (!adminAuthRequestResponse) {
return await this.clearExistingAdminAuthRequestAndStartNewRequest(userId);
}
// Request denied
if (adminAuthRequestResponse.isAnswered && !adminAuthRequestResponse.requestApproved) {
return await this.clearExistingAdminAuthRequestAndStartNewRequest(userId);
}
// Request approved
if (adminAuthRequestResponse.requestApproved) {
return await this.decryptViaApprovedAuthRequest(
adminAuthRequestResponse,
adminAuthRequestStorable.privateKey,
userId,
);
}
if (!this.email) {
this.logService.error("Email not defined when handling an existing an admin auth request.");
return;
}
// Re-derive the user's fingerprint phrase
// It is important to not use the server's public key here as it could have been compromised via MITM
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
adminAuthRequestStorable.privateKey,
);
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
derivedPublicKeyArrayBuffer,
);
// Request still pending response from admin set keypair and create hub connection
// so that any approvals will be received via push notification
this.authRequestKeyPair = {
privateKey: adminAuthRequestStorable.privateKey,
publicKey: undefined,
};
await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id);
}
/**
* This is used for trying to get the auth request back out of state.
* @param requestId
* @private
*/
private async retrieveAuthRequest(requestId: string): Promise<AuthRequestResponse> {
let authRequestResponse: AuthRequestResponse | undefined = undefined;
try {
// There are two cases here, the first being
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
// Get the response based on whether we've authenticated or not. We need to call a different API method
// based on whether we have a token or need to use the accessCode.
if (userHasAuthenticatedViaSSO) {
authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId);
} else {
if (!this.accessCode) {
const errorMessage = "No access code available when handling approved auth request.";
this.logService.error(errorMessage);
throw new Error(errorMessage);
}
authRequestResponse = await this.authRequestApiService.getAuthResponse(
requestId,
this.accessCode,
);
}
} catch (error) {
// If the request no longer exists, we treat it as if it's been answered (and denied).
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
authRequestResponse = undefined;
} else {
this.logService.error(error);
}
}
if (authRequestResponse === undefined) {
throw new Error("Auth request response not generated");
}
return authRequestResponse;
}
/**
* Determines if the Auth Request has been approved, deleted or denied, and handles
* the response accordingly.
* @param requestId The ID of the Auth Request to process
* @returns A boolean indicating whether the Auth Request was successfully processed
*/
private async handleExistingAuthRequestLogin(requestId: string): Promise<void> {
this.showResendNotification = false;
try {
const authRequestResponse = await this.retrieveAuthRequest(requestId);
// Request doesn't exist anymore, so we'll clear the cache and start a new request.
if (!authRequestResponse) {
return await this.clearExistingStandardAuthRequestAndStartNewRequest();
}
// Request denied, so we'll clear the cache and start a new request.
if (authRequestResponse.isAnswered && !authRequestResponse.requestApproved) {
return await this.clearExistingStandardAuthRequestAndStartNewRequest();
}
// Request approved, so we'll log the user in.
if (authRequestResponse.requestApproved) {
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
if (userHasAuthenticatedViaSSO) {
// [Standard Flow 3-4] Handle authenticated SSO TD user flows
return await this.handleAuthenticatedFlows(authRequestResponse);
} else {
// [Standard Flow 1-2] Handle unauthenticated user flows
return await this.handleUnauthenticatedFlows(authRequestResponse, requestId);
}
}
// At this point, we know that the request is still pending, so we'll start a hub connection to listen for a response.
await this.anonymousHubService.createHubConnection(requestId);
} catch (error) {
if (error instanceof ErrorResponse) {
await this.router.navigate([this.backToRoute]);
this.validationService.showError(error);
}
this.logService.error(error);
}
setTimeout(() => {
this.showResendNotification = true;
}, this.showResendNotificationTimeoutSeconds * 1000);
}
private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) {
// [Standard Flow 3-4] Handle authenticated SSO TD user flows
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (!userId) {
this.logService.error(
"Not able to get a user id from the account service active account observable.",
);
return;
}
if (!this.authRequestKeyPair || !this.authRequestKeyPair.privateKey) {
this.logService.error("No private key set when handling the authenticated flows.");
return;
}
await this.decryptViaApprovedAuthRequest(
authRequestResponse,
this.authRequestKeyPair.privateKey,
userId,
);
}
private async handleUnauthenticatedFlows(
authRequestResponse: AuthRequestResponse,
requestId: string,
) {
// [Standard Flow 1-2] Handle unauthenticated user flows
const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials(
requestId,
authRequestResponse,
);
if (!authRequestLoginCredentials) {
this.logService.error("Didn't set up auth request login credentials properly.");
return;
}
// Clear the cached auth request from state since we're using it to log in.
this.loginViaAuthRequestCacheService.clearCacheLoginView();
// Note: keys are set by AuthRequestLoginStrategy success handling
const authResult = await this.loginStrategyService.logIn(authRequestLoginCredentials);
await this.handlePostLoginNavigation(authResult);
}
private async decryptViaApprovedAuthRequest(
authRequestResponse: AuthRequestResponse,
privateKey: ArrayBuffer,
userId: UserId,
): Promise<void> {
/**
* [Flow Type Detection]
* We determine the type of `key` based on the presence or absence of `masterPasswordHash`:
* - If `masterPasswordHash` exists: Standard Flow 1 or 3 (device has masterKey)
* - If no `masterPasswordHash`: Standard Flow 2, 4, or Admin Flow (device sends userKey)
*/
if (authRequestResponse.masterPasswordHash) {
// [Standard Flow 1 or 3] Device has masterKey
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
authRequestResponse,
privateKey,
userId,
);
} else {
// [Standard Flow 2, 4, or Admin Flow] Device sends userKey
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
authRequestResponse,
privateKey,
userId,
);
}
// [Admin Flow Cleanup] Clear one-time use admin auth request
// clear the admin auth request from state so it cannot be used again (it's a one time use)
// TODO: this should eventually be enforced via deleting this on the server once it is used
await this.authRequestService.clearAdminAuthRequest(userId);
// [Standard Flow Cleanup] Clear the cached auth request from state
this.loginViaAuthRequestCacheService.clearCacheLoginView();
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("loginApproved"),
});
// [Device Trust] Establish trust if required
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
if (!activeAccount) {
this.logService.error("No active account defined from the account service.");
return;
}
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
await this.handleSuccessfulLoginNavigation(userId);
}
/**
* Takes an `AuthRequestResponse` and decrypts the `key` to build an `AuthRequestLoginCredentials`
* object for use in the `AuthRequestLoginStrategy`.
*
* The credentials object that gets built is affected by whether the `authRequestResponse.key`
* is an encrypted MasterKey or an encrypted UserKey.
*/
private async buildAuthRequestLoginCredentials(
requestId: string,
authRequestResponse: AuthRequestResponse,
): Promise<AuthRequestLoginCredentials | undefined> {
if (!this.authRequestKeyPair || !this.authRequestKeyPair.privateKey) {
this.logService.error("No private key set when building auth request login credentials.");
return;
}
if (!this.email) {
this.logService.error("Email not defined.");
return;
}
if (!this.accessCode) {
this.logService.error(
"Access code not defined when building auth request login credentials.",
);
return;
}
/**
* See verifyAndHandleApprovedAuthReq() for flow details.
*
* We determine the type of `key` based on the presence or absence of `masterPasswordHash`:
* - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)]
* - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey)
*/
if (authRequestResponse.masterPasswordHash) {
// ...in Standard Auth Request Flow 1
const { masterKey, masterKeyHash } =
await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash(
authRequestResponse.key,
authRequestResponse.masterPasswordHash,
this.authRequestKeyPair.privateKey,
);
return new AuthRequestLoginCredentials(
this.email,
this.accessCode,
requestId,
null, // no userKey
masterKey,
masterKeyHash,
);
} else {
// ...in Standard Auth Request Flow 2
const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey(
authRequestResponse.key,
this.authRequestKeyPair.privateKey,
);
return new AuthRequestLoginCredentials(
this.email,
this.accessCode,
requestId,
userKey,
null, // no masterKey
null, // no masterKeyHash
);
}
}
private async clearExistingAdminAuthRequestAndStartNewRequest(userId: UserId) {
// clear the admin auth request from state
await this.authRequestService.clearAdminAuthRequest(userId);
// start new auth request
await this.handleNewAdminAuthRequestLogin();
}
private async clearExistingStandardAuthRequestAndStartNewRequest(): Promise<void> {
// clear the auth request from state
this.loginViaAuthRequestCacheService.clearCacheLoginView();
// start new auth request
await this.handleNewStandardAuthRequestLogin();
}
private async handlePostLoginNavigation(loginResponse: AuthResult) {
if (loginResponse.requiresTwoFactor) {
await this.router.navigate(["2fa"]);
} else {
await this.handleSuccessfulLoginNavigation(loginResponse.userId);
}
}
private async handleSuccessfulLoginNavigation(userId: UserId) {
await this.loginSuccessHandlerService.run(userId);
await this.router.navigate(["vault"]);
}
}