mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
Login Flows (#4411)
* [SG-171] Login with a device request: Desktop (#3999) * Move LoginWithDeviceComponent to libs * Create login module * Remove login component from previous location * Move startPasswordlessLogin method to base class * Register route for login with device component * Add new localizations * Add Login with Device page styles * Add desktop login with device component * Spacing fix * Add content box around page * Update wording of helper text * Make resend timeout a class variable * SG-173 - Login device approval desktop (#4232) * SG-173 Implemented UI and login for login approval request * SG-173 - Show login approval after login * SG-173 Fetch login requests if the setting is true * SG-173 Add subheading to new setting * SG-173 Handle modal dismiss denying login request * SG-173 Fix pr comments * SG-173 Implemented desktop alerts * SG-173 Replicated behaviour of openViewRef * SG-173 Fixed previous commit * SG-173 PR fix * SG-173 Fix PR comment * SG-173 Added missing service injection * SG-173 Added logo to notifications * SG-173 Fix PR comments * [SG-910] Override self hosted check for desktop (#4405) * Override base component self hosted check * Add selfhost check to environment service * [SG-170] Login with Device Request - Browser (#4198) * work: ui stuff * fix: use parent * fix: words * [SG-987] [SG-988] [SG-989] Fix passwordless login request (#4573) * SG-987 Fix notification text and button options * SG-988 Fix approval and decline confirmation toasts * SG-989 Fix methods called * SG-988 Undo previous commit * [SG-1034] [Defect] - Vault is empty upon login confirmation (#4646) * fix: sync after login * undo: whoops --------- Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com> Co-authored-by: Brandon Maharaj <bmaharaj@bitwarden.com> Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
This commit is contained in:
210
libs/angular/src/components/login-with-device.component.ts
Normal file
210
libs/angular/src/components/login-with-device.component.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { LoginService } from "@bitwarden/common/abstractions/login.service";
|
||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||
import { AuthRequestType } from "@bitwarden/common/enums/authRequestType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { PasswordlessLogInCredentials } from "@bitwarden/common/models/domain/log-in-credentials";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/models/request/passwordless-create-auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/models/response/auth-request.response";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||
|
||||
@Directive()
|
||||
export class LoginWithDeviceComponent
|
||||
extends CaptchaProtectedComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private destroy$ = new Subject<void>();
|
||||
email: string;
|
||||
showResendNotification = false;
|
||||
passwordlessRequest: PasswordlessCreateAuthRequest;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
private resendTimeout = 12000;
|
||||
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
private cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private appIdService: AppIdService,
|
||||
private passwordGenerationService: PasswordGenerationService,
|
||||
private apiService: ApiService,
|
||||
private authService: AuthService,
|
||||
private logService: LogService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private anonymousHubService: AnonymousHubService,
|
||||
private validationService: ValidationService,
|
||||
private stateService: StateService,
|
||||
private loginService: LoginService
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
|
||||
const navigation = this.router.getCurrentNavigation();
|
||||
if (navigation) {
|
||||
this.email = navigation.extras?.state?.email;
|
||||
}
|
||||
|
||||
//gets signalR push notification
|
||||
this.authService
|
||||
.getPushNotifcationObs$()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => {
|
||||
this.confirmResponse(id);
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.email) {
|
||||
this.router.navigate(["/login"]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.startPasswordlessLogin();
|
||||
}
|
||||
|
||||
async startPasswordlessLogin() {
|
||||
this.showResendNotification = false;
|
||||
|
||||
try {
|
||||
await this.buildAuthRequest();
|
||||
const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
|
||||
|
||||
if (reqResponse.id) {
|
||||
this.anonymousHubService.createHubConnection(reqResponse.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.showResendNotification = true;
|
||||
}, this.resendTimeout);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.anonymousHubService.stopHubConnection();
|
||||
}
|
||||
|
||||
private async confirmResponse(requestId: string) {
|
||||
try {
|
||||
const response = await this.apiService.getAuthResponse(
|
||||
requestId,
|
||||
this.passwordlessRequest.accessCode
|
||||
);
|
||||
|
||||
if (!response.requestApproved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = await this.buildLoginCredntials(requestId, response);
|
||||
const loginResponse = await this.authService.logIn(credentials);
|
||||
|
||||
if (loginResponse.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.twoFactorRoute]);
|
||||
}
|
||||
} else if (loginResponse.forcePasswordReset) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||
this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.forcePasswordResetRoute]);
|
||||
}
|
||||
} else {
|
||||
await this.setRememberEmailValues();
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.router.navigate(["/login"]);
|
||||
this.validationService.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async setRememberEmailValues() {
|
||||
const rememberEmail = this.loginService.getRememberEmail();
|
||||
const rememberedEmail = this.loginService.getEmail();
|
||||
await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null);
|
||||
this.loginService.clearValues();
|
||||
}
|
||||
|
||||
private async buildAuthRequest() {
|
||||
this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
const fingerprint = await (
|
||||
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0])
|
||||
).join("-");
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
|
||||
|
||||
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
|
||||
this.email,
|
||||
deviceIdentifier,
|
||||
publicKey,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
accessCode,
|
||||
fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
private async buildLoginCredntials(
|
||||
requestId: string,
|
||||
response: AuthRequestResponse
|
||||
): Promise<PasswordlessLogInCredentials> {
|
||||
const decKey = await this.cryptoService.rsaDecrypt(response.key, this.authRequestKeyPair[1]);
|
||||
const decMasterPasswordHash = await this.cryptoService.rsaDecrypt(
|
||||
response.masterPasswordHash,
|
||||
this.authRequestKeyPair[1]
|
||||
);
|
||||
const key = new SymmetricCryptoKey(decKey);
|
||||
const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash);
|
||||
|
||||
return new PasswordlessLogInCredentials(
|
||||
this.email,
|
||||
this.passwordlessRequest.accessCode,
|
||||
requestId,
|
||||
key,
|
||||
localHashedPassword
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
private selfHosted = false;
|
||||
selfHosted = false;
|
||||
showLoginWithDevice: boolean;
|
||||
validatedEmail = false;
|
||||
paramEmailSet = false;
|
||||
@@ -176,6 +176,18 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
}
|
||||
}
|
||||
|
||||
async startPasswordlessLogin() {
|
||||
this.formGroup.get("masterPassword")?.clearValidators();
|
||||
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
||||
|
||||
if (!this.formGroup.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = this.formGroup.get("email").value;
|
||||
this.router.navigate(["/login-with-device"], { state: { email: email } });
|
||||
}
|
||||
|
||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||
await this.saveEmailSettings();
|
||||
// Generate necessary sso params
|
||||
@@ -259,7 +271,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
return `${error.controlName}${name}`;
|
||||
}
|
||||
|
||||
private async getLoginWithDevice(email: string) {
|
||||
async getLoginWithDevice(email: string) {
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const res = await this.apiService.getKnownDevice(email, deviceIdentifier);
|
||||
|
||||
Reference in New Issue
Block a user