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 { // 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 { 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 { 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 = this.accountService.activeAccount$.pipe(map((a) => a?.email)); const loginEmail$: Observable = 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 { this.toastService.showToast({ variant: "error", message: this.i18nService.t("userEmailMissing"), }); await this.router.navigate([this.backToRoute]); } async ngOnDestroy(): Promise { await this.anonymousHubService.stopHubConnection(); this.loginViaAuthRequestCacheService.clearCacheLoginView(); } private async handleNewAdminAuthRequestLogin(): Promise { 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 { // 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 { 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 { 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 { 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 { // 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 { 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 { 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 { /** * [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 { 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 { // 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"]); } }