mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[SG-168] Passwordless login web MVP (#3424)
* passwordless login page redesign * passwordless login page redesign * restyled login form to use tailwind * restyled login form to use tailwind * moved texts on login device template to locales * made reactive form changes for clients * added request model * made more changes * added implmentation to auth request api * fixed refrencing issue * renamed model property * Added resend notification functionality * Added new file * login with device first draft * login with device first draft * login with device first draft * login with device first draft * connection to anonymous hub * connection to anonymous hub * refactored confirm login response * removed comment * cleaned up login * changed uptyped form builder * changed uptyped form builder * [SG-168] Update login strategy with passwordless login credentials. * [SG-168] Removed logs. Changed inputs for passwordless logic strategy. Removed tokenRequestPasswordless it is using the same as password. * code cleanup * code cleanup * removed login with device from self hosted * fixed PR comments * added module for login * fixed post request bug * added feature flag * added feature flag * added feature flag Co-authored-by: André Bispo <abispo@bitwarden.com>
This commit is contained in:
@@ -10,5 +10,7 @@
|
||||
"port": 8080,
|
||||
"allowedHosts": "auto"
|
||||
},
|
||||
"flags": {}
|
||||
"flags": {
|
||||
"showPasswordless": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"proxyEvents": "https://events.bitwarden.com"
|
||||
},
|
||||
"flags": {
|
||||
"showTrial": true
|
||||
"showTrial": true,
|
||||
"showPasswordless": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"proxyNotifications": "http://localhost:61840"
|
||||
},
|
||||
"flags": {
|
||||
"showTrial": true
|
||||
"showTrial": true,
|
||||
"showPasswordless": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"proxyEvents": "https://events.qa.bitwarden.pw"
|
||||
},
|
||||
"flags": {
|
||||
"showTrial": true
|
||||
"showTrial": true,
|
||||
"showPasswordless": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"port": 8081
|
||||
},
|
||||
"flags": {
|
||||
"showTrial": false
|
||||
"showTrial": false,
|
||||
"showPasswordless": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<img class="mb-2 logo logo-themed" alt="Bitwarden" />
|
||||
<p class="lead text-center mx-4 mb-4">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="showResetPasswordAutoEnrollWarning"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<div class="d-flex">
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="MasterPassword"
|
||||
class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text">
|
||||
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
id="rememberEmail"
|
||||
name="RememberEmail"
|
||||
[(ngModel)]="rememberEmail"
|
||||
/>
|
||||
<label class="form-check-label" for="rememberEmail">{{ "rememberEmail" | i18n }}</label>
|
||||
</div>
|
||||
<div class="mb-n3" [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a
|
||||
routerLink="/register"
|
||||
[queryParams]="{ email: email }"
|
||||
class="btn btn-outline-secondary btn-block ml-2 mt-0"
|
||||
>
|
||||
<i class="bwi bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
|
||||
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,44 @@
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div>
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
<p class="tw-mx-4 tw-mt-3 tw-mb-4 tw-text-center tw-text-xl">
|
||||
{{ "loginOrCreateNewAccount" | i18n }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInInitiated" | i18n }}</h2>
|
||||
|
||||
<div class="tw-text-light">
|
||||
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
|
||||
|
||||
<p class="tw-mb-6">
|
||||
{{ "fingerprintMatchInfo" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<p>
|
||||
<code>{{ passwordlessRequest?.fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-my-10" *ngIf="showResendNotification">
|
||||
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
|
||||
"resendNotification" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-text-light tw-mt-3">
|
||||
{{ "loginWithDevciceEnabledInfo" | i18n }}
|
||||
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
175
apps/web/src/app/accounts/login/login-with-device.component.ts
Normal file
175
apps/web/src/app/accounts/login/login-with-device.component.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { CaptchaProtectedComponent } from "@bitwarden/angular/components/captchaProtected.component";
|
||||
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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { AuthRequestType } from "@bitwarden/common/enums/authRequestType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { PasswordlessLogInCredentials } from "@bitwarden/common/models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/models/request/passwordlessCreateAuthRequest";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/models/response/authRequestResponse";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-with-device",
|
||||
templateUrl: "login-with-device.component.html",
|
||||
})
|
||||
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>;
|
||||
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private appIdService: AppIdService,
|
||||
private passwordGenerationService: PasswordGenerationService,
|
||||
private apiService: ApiService,
|
||||
private authService: AuthService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private anonymousHubService: AnonymousHubService
|
||||
) {
|
||||
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;
|
||||
}, 12000);
|
||||
}
|
||||
|
||||
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);
|
||||
await this.authService.logIn(credentials);
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
121
apps/web/src/app/accounts/login/login.component.html
Normal file
121
apps/web/src/app/accounts/login/login.component.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="tw-container tw-mx-auto"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div>
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
<p class="tw-mx-4 tw-mt-3 tw-mb-4 tw-text-center tw-text-xl">
|
||||
{{ "loginOrCreateNewAccount" | i18n }}
|
||||
</p>
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<bit-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="showResetPasswordAutoEnrollWarning"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input id="login_input_email" bitInput type="email" formControlName="email" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="login_input_master-password"
|
||||
bitInput
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
formControlName="masterPassword"
|
||||
/>
|
||||
<button type="button" bitSuffix bitButton (click)="togglePassword()">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="bwi bwi-lg bwi-eye"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<bit-hint>
|
||||
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-items-start">
|
||||
<div class="tw-flex tw-h-6 tw-items-center">
|
||||
<input
|
||||
id="login_input_remember-email"
|
||||
class="tw-w-4 tw-rounded tw-border"
|
||||
bitInput
|
||||
type="checkbox"
|
||||
formControlName="rememberEmail"
|
||||
/>
|
||||
</div>
|
||||
<bit-label class="ml-2">
|
||||
{{ "rememberEmail" | i18n }}
|
||||
</bit-label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-space-x-4">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
class="tw-inline-block tw-w-1/2"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in"></i> {{ "logIn" | i18n }} </span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
routerLink="/register"
|
||||
class="tw-inline-block tw-w-1/2"
|
||||
>
|
||||
<i class="bwi bwi-pencil-square"></i>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3" *ngIf="!selfHosted && showPasswordless">
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
class="tw-w-full"
|
||||
(click)="startPasswordlessLogin()"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<a routerLink="/sso" bitButton buttonType="secondary" class="tw-w-full">
|
||||
<i class="bwi bwi-provider tw-mr-2"></i>
|
||||
{{ "enterpriseSingleSignOn" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
@@ -7,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||
@@ -20,7 +22,9 @@ import { Policy } from "@bitwarden/common/models/domain/policy";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/listResponse";
|
||||
import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse";
|
||||
|
||||
import { RouterService, StateService } from "../core";
|
||||
import { flagEnabled } from "src/utils/flags";
|
||||
|
||||
import { RouterService, StateService } from "../../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
@@ -31,6 +35,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: ListResponse<PolicyResponse>;
|
||||
showPasswordless = false;
|
||||
|
||||
constructor(
|
||||
authService: AuthService,
|
||||
@@ -48,7 +53,9 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
ngZone: NgZone,
|
||||
protected stateService: StateService,
|
||||
private messagingService: MessagingService,
|
||||
private routerService: RouterService
|
||||
private routerService: RouterService,
|
||||
formBuilder: FormBuilder,
|
||||
formValidationErrorService: FormValidationErrorsService
|
||||
) {
|
||||
super(
|
||||
authService,
|
||||
@@ -60,19 +67,22 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
passwordGenerationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
ngZone
|
||||
ngZone,
|
||||
formBuilder,
|
||||
formValidationErrorService
|
||||
);
|
||||
this.onSuccessfulLogin = async () => {
|
||||
this.messagingService.send("setFullWidth");
|
||||
};
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
this.showPasswordless = flagEnabled("showPasswordless");
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
this.formGroup.get("email")?.setValue(qParams.email);
|
||||
}
|
||||
if (qParams.premium != null) {
|
||||
this.routerService.setPreviousUrl("/settings/premium");
|
||||
@@ -91,7 +101,8 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
await super.ngOnInit();
|
||||
this.rememberEmail = await this.stateService.getRememberEmail();
|
||||
const rememberEmail = await this.stateService.getRememberEmail();
|
||||
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
|
||||
});
|
||||
|
||||
const invite = await this.stateService.getOrganizationInvitation();
|
||||
@@ -125,10 +136,12 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
}
|
||||
|
||||
async goAfterLogIn() {
|
||||
const masterPassword = this.formGroup.get("masterPassword")?.value;
|
||||
|
||||
// Check master password against policy
|
||||
if (this.enforcedPasswordPolicyOptions != null) {
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
this.masterPassword,
|
||||
masterPassword,
|
||||
this.getPasswordStrengthUserInput()
|
||||
);
|
||||
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
@@ -137,7 +150,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
if (
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
masterPasswordScore,
|
||||
this.masterPassword,
|
||||
masterPassword,
|
||||
this.enforcedPasswordPolicyOptions
|
||||
)
|
||||
) {
|
||||
@@ -158,19 +171,34 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.stateService.setRememberEmail(this.rememberEmail);
|
||||
if (!this.rememberEmail) {
|
||||
const rememberEmail = this.formGroup.get("rememberEmail")?.value;
|
||||
|
||||
await this.stateService.setRememberEmail(rememberEmail);
|
||||
if (!rememberEmail) {
|
||||
await this.stateService.setRememberedEmail(null);
|
||||
}
|
||||
await super.submit();
|
||||
await super.submit(false);
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
const email = this.formGroup.get("email")?.value;
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf("@");
|
||||
const atPosition = email.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(
|
||||
this.email
|
||||
email
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
13
apps/web/src/app/accounts/login/login.module.ts
Normal file
13
apps/web/src/app/accounts/login/login.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { LoginWithDeviceComponent } from "./login-with-device.component";
|
||||
import { LoginComponent } from "./login.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [LoginComponent, LoginWithDeviceComponent],
|
||||
exports: [LoginComponent, LoginWithDeviceComponent],
|
||||
})
|
||||
export class LoginModule {}
|
||||
@@ -11,7 +11,8 @@ import { AcceptEmergencyComponent } from "./accounts/accept-emergency.component"
|
||||
import { AcceptOrganizationComponent } from "./accounts/accept-organization.component";
|
||||
import { HintComponent } from "./accounts/hint.component";
|
||||
import { LockComponent } from "./accounts/lock.component";
|
||||
import { LoginComponent } from "./accounts/login.component";
|
||||
import { LoginWithDeviceComponent } from "./accounts/login/login-with-device.component";
|
||||
import { LoginComponent } from "./accounts/login/login.component";
|
||||
import { RecoverDeleteComponent } from "./accounts/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component";
|
||||
import { RegisterComponent } from "./accounts/register.component";
|
||||
@@ -60,6 +61,11 @@ const routes: Routes = [
|
||||
canActivate: [HomeGuard], // Redirects either to vault, login or lock page.
|
||||
},
|
||||
{ path: "login", component: LoginComponent, canActivate: [UnauthGuard] },
|
||||
{
|
||||
path: "login-with-device",
|
||||
component: LoginWithDeviceComponent,
|
||||
data: { titleId: "loginWithDevice" },
|
||||
},
|
||||
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
|
||||
{
|
||||
path: "register",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LoginModule } from "./accounts/login/login.module";
|
||||
import { TrialInitiationModule } from "./accounts/trial-initiation/trial-initiation.module";
|
||||
import { OrganizationCreateModule } from "./organizations/create/organization-create.module";
|
||||
import { OrganizationManageModule } from "./organizations/manage/organization-manage.module";
|
||||
@@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
|
||||
OrganizationManageModule,
|
||||
OrganizationUserModule,
|
||||
OrganizationCreateModule,
|
||||
LoginModule,
|
||||
],
|
||||
exports: [
|
||||
SharedModule,
|
||||
@@ -25,6 +27,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
|
||||
TrialInitiationModule,
|
||||
VaultFilterModule,
|
||||
OrganizationBadgeModule,
|
||||
LoginModule,
|
||||
],
|
||||
bootstrap: [],
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ import { AcceptEmergencyComponent } from "../accounts/accept-emergency.component
|
||||
import { AcceptOrganizationComponent } from "../accounts/accept-organization.component";
|
||||
import { HintComponent } from "../accounts/hint.component";
|
||||
import { LockComponent } from "../accounts/lock.component";
|
||||
import { LoginComponent } from "../accounts/login.component";
|
||||
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
|
||||
import { RegisterFormModule } from "../accounts/register-form/register-form.module";
|
||||
@@ -210,7 +209,6 @@ import { SharedModule } from ".";
|
||||
FrontendLayoutComponent,
|
||||
HintComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
NavbarComponent,
|
||||
NestedCheckboxComponent,
|
||||
@@ -355,7 +353,6 @@ import { SharedModule } from ".";
|
||||
FrontendLayoutComponent,
|
||||
HintComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
NavbarComponent,
|
||||
NestedCheckboxComponent,
|
||||
|
||||
@@ -569,15 +569,27 @@
|
||||
"loginOrCreateNewAccount": {
|
||||
"message": "Log in or create a new account to access your secure vault."
|
||||
},
|
||||
"loginWithDevice" : {
|
||||
"message": "Log in with device"
|
||||
},
|
||||
"loginWithDevciceEnabledInfo": {
|
||||
"message": "Log in with device must be enabled in the settings of the Biwarden mobile app. Need another option?"
|
||||
},
|
||||
"createAccount": {
|
||||
"message": "Create Account"
|
||||
},
|
||||
"newAroundHere": {
|
||||
"message": "New around here?"
|
||||
},
|
||||
"startTrial": {
|
||||
"message": "Start Trial"
|
||||
},
|
||||
"logIn": {
|
||||
"message": "Log In"
|
||||
},
|
||||
"logInInitiated": {
|
||||
"message": "Log in initiated"
|
||||
},
|
||||
"submit": {
|
||||
"message": "Submit"
|
||||
},
|
||||
@@ -635,7 +647,7 @@
|
||||
"confirmMasterPasswordRequired": {
|
||||
"message": "Master password retype is required."
|
||||
},
|
||||
"masterPasswordMinLength": {
|
||||
"masterPasswordMinlength": {
|
||||
"message": "Master password must be at least 8 characters long."
|
||||
},
|
||||
"masterPassDoesntMatch": {
|
||||
@@ -705,6 +717,9 @@
|
||||
"noOrganizationsList": {
|
||||
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users."
|
||||
},
|
||||
"notificationSentDevice":{
|
||||
"message": "A notification has been sent to your device."
|
||||
},
|
||||
"versionNumber": {
|
||||
"message": "Version $VERSION_NUMBER$",
|
||||
"placeholders": {
|
||||
@@ -2532,6 +2547,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "View all log in options"
|
||||
},
|
||||
"viewedItemId": {
|
||||
"message": "Viewed item $ID$.",
|
||||
"placeholders": {
|
||||
@@ -3372,6 +3390,12 @@
|
||||
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device."
|
||||
},
|
||||
"fingerprintPhraseHeader": {
|
||||
"message": "Fingerprint phrase"
|
||||
},
|
||||
"dontAskFingerprintAgain": {
|
||||
"message": "Never prompt to verify fingerprint phrases for invited users (Not recommended)",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
@@ -4372,6 +4396,9 @@
|
||||
"reinviteSelected": {
|
||||
"message": "Resend Invitations"
|
||||
},
|
||||
"resendNotification": {
|
||||
"message": "Resend notification"
|
||||
},
|
||||
"noSelectedUsersApplicable": {
|
||||
"message": "This action is not applicable to any of the selected users."
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-types */
|
||||
export type Flags = {
|
||||
showTrial?: boolean;
|
||||
showPasswordless?: boolean;
|
||||
} & SharedFlags;
|
||||
|
||||
// required to avoid linting errors when there are no flags
|
||||
|
||||
Reference in New Issue
Block a user