|
|
|
|
@@ -0,0 +1,569 @@
|
|
|
|
|
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 { firstValueFrom, map } from "rxjs";
|
|
|
|
|
|
|
|
|
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|
|
|
|
import {
|
|
|
|
|
AuthRequestLoginCredentials,
|
|
|
|
|
AuthRequestServiceAbstraction,
|
|
|
|
|
LoginEmailServiceAbstraction,
|
|
|
|
|
LoginStrategyServiceAbstraction,
|
|
|
|
|
} 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
|
|
|
|
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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
|
|
|
|
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
|
|
|
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
|
|
|
|
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
|
|
|
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
|
|
|
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
|
|
|
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.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";
|
|
|
|
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
|
|
|
|
import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
|
|
|
|
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
|
|
|
|
|
|
|
|
|
import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service";
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
|
standalone: true,
|
|
|
|
|
templateUrl: "./login-via-auth-request.component.html",
|
|
|
|
|
imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule],
|
|
|
|
|
})
|
|
|
|
|
export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|
|
|
|
private authRequest: AuthRequest;
|
|
|
|
|
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
|
|
|
|
|
private authStatus: AuthenticationStatus;
|
|
|
|
|
private showResendNotificationTimeoutSeconds = 12;
|
|
|
|
|
|
|
|
|
|
protected backToRoute = "/login";
|
|
|
|
|
protected clientType: ClientType;
|
|
|
|
|
protected ClientType = ClientType;
|
|
|
|
|
protected email: string;
|
|
|
|
|
protected fingerprintPhrase: string;
|
|
|
|
|
protected showResendNotification = false;
|
|
|
|
|
protected Flow = Flow;
|
|
|
|
|
protected flow = Flow.StandardAuthRequest;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private accountService: AccountService,
|
|
|
|
|
private anonymousHubService: AnonymousHubService,
|
|
|
|
|
private appIdService: AppIdService,
|
|
|
|
|
private authRequestApiService: AuthRequestApiService,
|
|
|
|
|
private authRequestService: AuthRequestServiceAbstraction,
|
|
|
|
|
private authService: AuthService,
|
|
|
|
|
private cryptoFunctionService: CryptoFunctionService,
|
|
|
|
|
private deviceTrustService: DeviceTrustServiceAbstraction,
|
|
|
|
|
private i18nService: I18nService,
|
|
|
|
|
private logService: LogService,
|
|
|
|
|
private loginEmailService: LoginEmailServiceAbstraction,
|
|
|
|
|
private loginStrategyService: LoginStrategyServiceAbstraction,
|
|
|
|
|
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
|
|
|
|
private platformUtilsService: PlatformUtilsService,
|
|
|
|
|
private router: Router,
|
|
|
|
|
private syncService: SyncService,
|
|
|
|
|
private toastService: ToastService,
|
|
|
|
|
private validationService: ValidationService,
|
|
|
|
|
) {
|
|
|
|
|
this.clientType = this.platformUtilsService.getClientType();
|
|
|
|
|
|
|
|
|
|
// Gets SignalR push notification
|
|
|
|
|
// Only fires on approval to prevent enumeration
|
|
|
|
|
this.authRequestService.authRequestPushNotification$
|
|
|
|
|
.pipe(takeUntilDestroyed())
|
|
|
|
|
.subscribe((requestId) => {
|
|
|
|
|
this.verifyAndHandleApprovedAuthReq(requestId).catch((e: Error) => {
|
|
|
|
|
this.toastService.showToast({
|
|
|
|
|
variant: "error",
|
|
|
|
|
title: this.i18nService.t("error"),
|
|
|
|
|
message: e.message,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.logService.error("Failed to use approved auth request: " + e.message);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async initAdminAuthRequestFlow(): Promise<void> {
|
|
|
|
|
this.flow = Flow.AdminAuthRequest;
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We only allow a single admin approval request to be active at a time
|
|
|
|
|
// so we must check state to see if we have an existing one or not
|
|
|
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
|
|
|
|
const existingAdminAuthRequest = await this.authRequestService.getAdminAuthRequest(userId);
|
|
|
|
|
|
|
|
|
|
if (existingAdminAuthRequest) {
|
|
|
|
|
await this.handleExistingAdminAuthRequest(existingAdminAuthRequest, userId);
|
|
|
|
|
} else {
|
|
|
|
|
await this.startAdminAuthRequestLogin();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async initStandardAuthRequestFlow(): Promise<void> {
|
|
|
|
|
this.flow = Flow.StandardAuthRequest;
|
|
|
|
|
|
|
|
|
|
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
|
|
|
|
|
|
|
|
|
if (!this.email) {
|
|
|
|
|
await this.handleMissingEmail();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.startStandardAuthRequestLogin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleMissingEmail(): Promise<void> {
|
|
|
|
|
this.toastService.showToast({
|
|
|
|
|
variant: "error",
|
|
|
|
|
title: null,
|
|
|
|
|
message: this.i18nService.t("userEmailMissing"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.router.navigate([this.backToRoute]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async ngOnDestroy(): Promise<void> {
|
|
|
|
|
await this.anonymousHubService.stopHubConnection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async startAdminAuthRequestLogin(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.buildAuthRequest(AuthRequestType.AdminApproval);
|
|
|
|
|
|
|
|
|
|
const authRequestResponse = await this.authRequestApiService.postAdminAuthRequest(
|
|
|
|
|
this.authRequest,
|
|
|
|
|
);
|
|
|
|
|
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
|
|
|
|
id: authRequestResponse.id,
|
|
|
|
|
privateKey: this.authRequestKeyPair.privateKey,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
|
|
|
|
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
|
|
|
|
|
|
|
|
|
|
if (authRequestResponse.id) {
|
|
|
|
|
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.logService.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected async startStandardAuthRequestLogin(): Promise<void> {
|
|
|
|
|
this.showResendNotification = false;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
|
|
|
|
|
|
|
|
|
const authRequestResponse = await this.authRequestApiService.postAuthRequest(
|
|
|
|
|
this.authRequest,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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(authRequestType: AuthRequestType): Promise<void> {
|
|
|
|
|
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
|
|
|
|
|
|
|
|
|
this.authRequestKeyPair = {
|
|
|
|
|
publicKey: authRequestKeyPairArray[0],
|
|
|
|
|
privateKey: authRequestKeyPairArray[1],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deviceIdentifier = await this.appIdService.getAppId();
|
|
|
|
|
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
|
|
|
|
const accessCode = await this.passwordGenerationService.generatePassword({
|
|
|
|
|
type: "password",
|
|
|
|
|
length: 25,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
|
|
|
|
this.email,
|
|
|
|
|
this.authRequestKeyPair.publicKey,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.authRequest = new AuthRequest(
|
|
|
|
|
this.email,
|
|
|
|
|
deviceIdentifier,
|
|
|
|
|
publicKey,
|
|
|
|
|
authRequestType,
|
|
|
|
|
accessCode,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleExistingAdminAuthRequest(
|
|
|
|
|
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.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Request doesn't exist anymore
|
|
|
|
|
if (!adminAuthRequestResponse) {
|
|
|
|
|
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 denied
|
|
|
|
|
if (adminAuthRequestResponse.isAnswered && !adminAuthRequestResponse.requestApproved) {
|
|
|
|
|
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Request approved
|
|
|
|
|
if (adminAuthRequestResponse.requestApproved) {
|
|
|
|
|
return await this.decryptViaApprovedAuthRequest(
|
|
|
|
|
adminAuthRequestResponse,
|
|
|
|
|
adminAuthRequestStorable.privateKey,
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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: null };
|
|
|
|
|
await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async verifyAndHandleApprovedAuthReq(requestId: string): Promise<void> {
|
|
|
|
|
/**
|
|
|
|
|
* ***********************************
|
|
|
|
|
* Standard Auth Request Flows
|
|
|
|
|
* ***********************************
|
|
|
|
|
*
|
|
|
|
|
* Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory.
|
|
|
|
|
*
|
|
|
|
|
* Unauthed user clicks "Login with device" > navigates to /login-with-device which creates a StandardAuthRequest
|
|
|
|
|
* > receives approval from a device with authRequestPublicKey(masterKey) > decrypts masterKey > decrypts userKey > proceed to vault
|
|
|
|
|
*
|
|
|
|
|
* Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory.
|
|
|
|
|
*
|
|
|
|
|
* Unauthed user clicks "Login with device" > navigates to /login-with-device which creates a StandardAuthRequest
|
|
|
|
|
* > receives approval from a device with authRequestPublicKey(userKey) > decrypts userKey > proceeds to vault
|
|
|
|
|
*
|
|
|
|
|
* Note: this flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could get into this flow:
|
|
|
|
|
* 1) An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey in memory.
|
|
|
|
|
* 2) The org admin...
|
|
|
|
|
* (2a) Changes the member decryption options from "Trusted devices" to "Master password" AND
|
|
|
|
|
* (2b) Turns off the "Require single sign-on authentication" policy
|
|
|
|
|
* 3) On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO.
|
|
|
|
|
* 4) The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in memory (see step 1 above).
|
|
|
|
|
*
|
|
|
|
|
* Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory.
|
|
|
|
|
*
|
|
|
|
|
* SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Approve from your other device"
|
|
|
|
|
* > navigates to /login-with-device which creates a StandardAuthRequest > receives approval from device with authRequestPublicKey(masterKey)
|
|
|
|
|
* > decrypts masterKey > decrypts userKey > establishes trust (if required) > proceeds to vault
|
|
|
|
|
*
|
|
|
|
|
* Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory.
|
|
|
|
|
*
|
|
|
|
|
* SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Approve from your other device"
|
|
|
|
|
* > navigates to /login-with-device which creates a StandardAuthRequest > receives approval from device with authRequestPublicKey(userKey)
|
|
|
|
|
* > decrypts userKey > establishes trust (if required) > proceeds to vault
|
|
|
|
|
*
|
|
|
|
|
* ***********************************
|
|
|
|
|
* Admin Auth Request Flow
|
|
|
|
|
* ***********************************
|
|
|
|
|
*
|
|
|
|
|
* Flow: Authed SSO TD user requests admin approval.
|
|
|
|
|
*
|
|
|
|
|
* SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Request admin approval"
|
|
|
|
|
* > navigates to /admin-approval-requested which creates an AdminAuthRequest > receives approval from device with authRequestPublicKey(userKey)
|
|
|
|
|
* > decrypts userKey > establishes trust (if required) > proceeds to vault
|
|
|
|
|
*
|
|
|
|
|
* Note: TDE users are required to be enrolled in admin password reset, which gives the admin access to the user's userKey.
|
|
|
|
|
* This is how admins are able to send over the authRequestPublicKey(userKey) to the user to allow them to unlock.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* Summary Table
|
|
|
|
|
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
|
|
|
* | Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory (see note 1) |
|
|
|
|
|
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
|
|
|
* | Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes |
|
|
|
|
|
* | Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no |
|
|
|
|
|
* | Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes |
|
|
|
|
|
* | Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no | |
|
|
|
|
|
* | Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey |
|
|
|
|
|
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
|
|
|
* * Note 1: The phrase "in memory" here is important. It is possible for a user to have a master password for their account, but not have a masterKey IN MEMORY for
|
|
|
|
|
* a specific device. For example, if a user registers an account with a master password, then joins an SSO TD org, then logs in to a device via SSO and
|
|
|
|
|
* admin auth request, they are now logged into that device but that device does not have masterKey IN MEMORY.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
|
|
|
|
|
|
|
|
|
if (userHasAuthenticatedViaSSO) {
|
|
|
|
|
// Get the auth request from the server
|
|
|
|
|
// User is authenticated, therefore the endpoint does not require an access code.
|
|
|
|
|
const authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId);
|
|
|
|
|
|
|
|
|
|
if (authRequestResponse.requestApproved) {
|
|
|
|
|
// Handles Standard Flows 3-4 and Admin Flow
|
|
|
|
|
await this.handleAuthenticatedFlows(authRequestResponse);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Get the auth request from the server
|
|
|
|
|
// User is unauthenticated, therefore the endpoint requires an access code for user verification.
|
|
|
|
|
const authRequestResponse = await this.authRequestApiService.getAuthResponse(
|
|
|
|
|
requestId,
|
|
|
|
|
this.authRequest.accessCode,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (authRequestResponse.requestApproved) {
|
|
|
|
|
// Handles Standard Flows 1-2
|
|
|
|
|
await this.handleUnauthenticatedFlows(authRequestResponse, requestId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof ErrorResponse) {
|
|
|
|
|
await this.router.navigate([this.backToRoute]);
|
|
|
|
|
this.validationService.showError(error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.logService.error(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) {
|
|
|
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
|
|
|
|
|
|
|
|
|
await this.decryptViaApprovedAuthRequest(
|
|
|
|
|
authRequestResponse,
|
|
|
|
|
this.authRequestKeyPair.privateKey,
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleUnauthenticatedFlows(
|
|
|
|
|
authRequestResponse: AuthRequestResponse,
|
|
|
|
|
requestId: string,
|
|
|
|
|
) {
|
|
|
|
|
const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials(
|
|
|
|
|
requestId,
|
|
|
|
|
authRequestResponse,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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> {
|
|
|
|
|
/**
|
|
|
|
|
* 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 3
|
|
|
|
|
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
|
|
|
|
authRequestResponse,
|
|
|
|
|
privateKey,
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// ...in Standard Auth Request Flow 4 or Admin Auth Request Flow
|
|
|
|
|
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
|
|
|
|
|
authRequestResponse,
|
|
|
|
|
privateKey,
|
|
|
|
|
userId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
this.toastService.showToast({
|
|
|
|
|
variant: "success",
|
|
|
|
|
title: null,
|
|
|
|
|
message: this.i18nService.t("loginApproved"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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$);
|
|
|
|
|
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
|
|
|
|
|
|
|
|
|
await this.handleSuccessfulLoginNavigation();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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> {
|
|
|
|
|
/**
|
|
|
|
|
* 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.authRequest.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.authRequest.accessCode,
|
|
|
|
|
requestId,
|
|
|
|
|
userKey,
|
|
|
|
|
null, // no masterKey
|
|
|
|
|
null, // no masterKeyHash
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) {
|
|
|
|
|
// clear the admin auth request from state
|
|
|
|
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
|
|
|
|
|
|
|
|
|
// start new auth request
|
|
|
|
|
await this.startAdminAuthRequestLogin();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handlePostLoginNavigation(loginResponse: AuthResult) {
|
|
|
|
|
if (loginResponse.requiresTwoFactor) {
|
|
|
|
|
await this.router.navigate(["2fa"]);
|
|
|
|
|
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
|
|
|
|
|
await this.router.navigate(["update-temp-password"]);
|
|
|
|
|
} else {
|
|
|
|
|
await this.handleSuccessfulLoginNavigation();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleSuccessfulLoginNavigation() {
|
|
|
|
|
if (this.flow === Flow.StandardAuthRequest) {
|
|
|
|
|
// Only need to set remembered email on standard login with auth req flow
|
|
|
|
|
await this.loginEmailService.saveEmailSettings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.syncService.fullSync(true);
|
|
|
|
|
await this.router.navigate(["vault"]);
|
|
|
|
|
}
|
|
|
|
|
}
|