[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:
parent
debaef2941
commit
22a878792e
|
@ -1,4 +1,4 @@
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
|
||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/home">{{ "cancel" | i18n }}</button>
|
<button type="button" routerLink="/home">{{ "cancel" | i18n }}</button>
|
||||||
|
@ -18,15 +18,7 @@
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="box-content-row" appBoxRow>
|
<div class="box-content-row" appBoxRow>
|
||||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||||
<input
|
<input id="email" type="email" formControlName="email" appInputVerbatim="false" />
|
||||||
id="email"
|
|
||||||
type="text"
|
|
||||||
name="Email"
|
|
||||||
[(ngModel)]="email"
|
|
||||||
required
|
|
||||||
inputmode="email"
|
|
||||||
appInputVerbatim="false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
|
@ -34,10 +26,8 @@
|
||||||
<input
|
<input
|
||||||
id="masterPassword"
|
id="masterPassword"
|
||||||
type="{{ showPassword ? 'text' : 'password' }}"
|
type="{{ showPassword ? 'text' : 'password' }}"
|
||||||
name="MasterPassword"
|
|
||||||
class="monospaced"
|
class="monospaced"
|
||||||
[(ngModel)]="masterPassword"
|
formControlName="masterPassword"
|
||||||
required
|
|
||||||
appInputVerbatim
|
appInputVerbatim
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { Component, NgZone } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
|
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
|
||||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.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 { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
|
@ -30,7 +32,9 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
syncService: SyncService,
|
syncService: SyncService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
ngZone: NgZone
|
ngZone: NgZone,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
formValidationErrorService: FormValidationErrorsService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
authService,
|
authService,
|
||||||
|
@ -42,7 +46,9 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
logService,
|
logService,
|
||||||
ngZone
|
ngZone,
|
||||||
|
formBuilder,
|
||||||
|
formValidationErrorService
|
||||||
);
|
);
|
||||||
super.onSuccessfulLogin = async () => {
|
super.onSuccessfulLogin = async () => {
|
||||||
await syncService.fullSync(true);
|
await syncService.fullSync(true);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#form
|
#form
|
||||||
(ngSubmit)="submit()"
|
(ngSubmit)="submit()"
|
||||||
[appApiAction]="formPromise"
|
[appApiAction]="formPromise"
|
||||||
|
[formGroup]="formGroup"
|
||||||
attr.aria-hidden="{{ showingModal }}"
|
attr.aria-hidden="{{ showingModal }}"
|
||||||
>
|
>
|
||||||
<div id="content" class="content">
|
<div id="content" class="content">
|
||||||
|
@ -25,14 +26,7 @@
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="box-content-row" appBoxRow>
|
<div class="box-content-row" appBoxRow>
|
||||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||||
<input
|
<input id="email" type="email" formControlName="email" appInputVerbatim="false" />
|
||||||
id="email"
|
|
||||||
type="text"
|
|
||||||
name="Email"
|
|
||||||
[(ngModel)]="email"
|
|
||||||
required
|
|
||||||
appInputVerbatim="false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
|
@ -40,10 +34,8 @@
|
||||||
<input
|
<input
|
||||||
id="masterPassword"
|
id="masterPassword"
|
||||||
type="{{ showPassword ? 'text' : 'password' }}"
|
type="{{ showPassword ? 'text' : 'password' }}"
|
||||||
name="MasterPassword"
|
|
||||||
class="monospaced"
|
class="monospaced"
|
||||||
[(ngModel)]="masterPassword"
|
formControlName="masterPassword"
|
||||||
required
|
|
||||||
appInputVerbatim
|
appInputVerbatim
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
|
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
|
||||||
|
@ -7,6 +8,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.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 { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||||
|
@ -47,7 +49,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
ngZone: NgZone,
|
ngZone: NgZone,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
logService: LogService
|
logService: LogService,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
formValidationErrorService: FormValidationErrorsService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
authService,
|
authService,
|
||||||
|
@ -59,7 +63,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
logService,
|
logService,
|
||||||
ngZone
|
ngZone,
|
||||||
|
formBuilder,
|
||||||
|
formValidationErrorService
|
||||||
);
|
);
|
||||||
super.onSuccessfulLogin = () => {
|
super.onSuccessfulLogin = () => {
|
||||||
return syncService.fullSync(true);
|
return syncService.fullSync(true);
|
||||||
|
|
|
@ -10,5 +10,7 @@
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"allowedHosts": "auto"
|
"allowedHosts": "auto"
|
||||||
},
|
},
|
||||||
"flags": {}
|
"flags": {
|
||||||
|
"showPasswordless": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"proxyEvents": "https://events.bitwarden.com"
|
"proxyEvents": "https://events.bitwarden.com"
|
||||||
},
|
},
|
||||||
"flags": {
|
"flags": {
|
||||||
"showTrial": true
|
"showTrial": true,
|
||||||
|
"showPasswordless": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"proxyNotifications": "http://localhost:61840"
|
"proxyNotifications": "http://localhost:61840"
|
||||||
},
|
},
|
||||||
"flags": {
|
"flags": {
|
||||||
"showTrial": true
|
"showTrial": true,
|
||||||
|
"showPasswordless": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"proxyEvents": "https://events.qa.bitwarden.pw"
|
"proxyEvents": "https://events.qa.bitwarden.pw"
|
||||||
},
|
},
|
||||||
"flags": {
|
"flags": {
|
||||||
"showTrial": true
|
"showTrial": true,
|
||||||
|
"showPasswordless": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"port": 8081
|
"port": 8081
|
||||||
},
|
},
|
||||||
"flags": {
|
"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>
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { Component, NgZone } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { first } from "rxjs/operators";
|
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 { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.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 { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.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 { ListResponse } from "@bitwarden/common/models/response/listResponse";
|
||||||
import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse";
|
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({
|
@Component({
|
||||||
selector: "app-login",
|
selector: "app-login",
|
||||||
|
@ -31,6 +35,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
showResetPasswordAutoEnrollWarning = false;
|
showResetPasswordAutoEnrollWarning = false;
|
||||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
policies: ListResponse<PolicyResponse>;
|
policies: ListResponse<PolicyResponse>;
|
||||||
|
showPasswordless = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
|
@ -48,7 +53,9 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
ngZone: NgZone,
|
ngZone: NgZone,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private routerService: RouterService
|
private routerService: RouterService,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
formValidationErrorService: FormValidationErrorsService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
authService,
|
authService,
|
||||||
|
@ -60,19 +67,22 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
logService,
|
logService,
|
||||||
ngZone
|
ngZone,
|
||||||
|
formBuilder,
|
||||||
|
formValidationErrorService
|
||||||
);
|
);
|
||||||
this.onSuccessfulLogin = async () => {
|
this.onSuccessfulLogin = async () => {
|
||||||
this.messagingService.send("setFullWidth");
|
this.messagingService.send("setFullWidth");
|
||||||
};
|
};
|
||||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||||
|
this.showPasswordless = flagEnabled("showPasswordless");
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||||
this.email = qParams.email;
|
this.formGroup.get("email")?.setValue(qParams.email);
|
||||||
}
|
}
|
||||||
if (qParams.premium != null) {
|
if (qParams.premium != null) {
|
||||||
this.routerService.setPreviousUrl("/settings/premium");
|
this.routerService.setPreviousUrl("/settings/premium");
|
||||||
|
@ -91,7 +101,8 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
this.routerService.setPreviousUrl(route.toString());
|
this.routerService.setPreviousUrl(route.toString());
|
||||||
}
|
}
|
||||||
await super.ngOnInit();
|
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();
|
const invite = await this.stateService.getOrganizationInvitation();
|
||||||
|
@ -125,10 +136,12 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async goAfterLogIn() {
|
async goAfterLogIn() {
|
||||||
|
const masterPassword = this.formGroup.get("masterPassword")?.value;
|
||||||
|
|
||||||
// Check master password against policy
|
// Check master password against policy
|
||||||
if (this.enforcedPasswordPolicyOptions != null) {
|
if (this.enforcedPasswordPolicyOptions != null) {
|
||||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||||
this.masterPassword,
|
masterPassword,
|
||||||
this.getPasswordStrengthUserInput()
|
this.getPasswordStrengthUserInput()
|
||||||
);
|
);
|
||||||
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||||
|
@ -137,7 +150,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
if (
|
if (
|
||||||
!this.policyService.evaluateMasterPassword(
|
!this.policyService.evaluateMasterPassword(
|
||||||
masterPasswordScore,
|
masterPasswordScore,
|
||||||
this.masterPassword,
|
masterPassword,
|
||||||
this.enforcedPasswordPolicyOptions
|
this.enforcedPasswordPolicyOptions
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -158,19 +171,34 @@ export class LoginComponent extends BaseLoginComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
await this.stateService.setRememberEmail(this.rememberEmail);
|
const rememberEmail = this.formGroup.get("rememberEmail")?.value;
|
||||||
if (!this.rememberEmail) {
|
|
||||||
|
await this.stateService.setRememberEmail(rememberEmail);
|
||||||
|
if (!rememberEmail) {
|
||||||
await this.stateService.setRememberedEmail(null);
|
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() {
|
private getPasswordStrengthUserInput() {
|
||||||
|
const email = this.formGroup.get("email")?.value;
|
||||||
let userInput: string[] = [];
|
let userInput: string[] = [];
|
||||||
const atPosition = this.email.indexOf("@");
|
const atPosition = email.indexOf("@");
|
||||||
if (atPosition > -1) {
|
if (atPosition > -1) {
|
||||||
userInput = userInput.concat(
|
userInput = userInput.concat(
|
||||||
this.email
|
email
|
||||||
.substr(0, atPosition)
|
.substr(0, atPosition)
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
|
@ -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 { AcceptOrganizationComponent } from "./accounts/accept-organization.component";
|
||||||
import { HintComponent } from "./accounts/hint.component";
|
import { HintComponent } from "./accounts/hint.component";
|
||||||
import { LockComponent } from "./accounts/lock.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 { RecoverDeleteComponent } from "./accounts/recover-delete.component";
|
||||||
import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component";
|
import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component";
|
||||||
import { RegisterComponent } from "./accounts/register.component";
|
import { RegisterComponent } from "./accounts/register.component";
|
||||||
|
@ -60,6 +61,11 @@ const routes: Routes = [
|
||||||
canActivate: [HomeGuard], // Redirects either to vault, login or lock page.
|
canActivate: [HomeGuard], // Redirects either to vault, login or lock page.
|
||||||
},
|
},
|
||||||
{ path: "login", component: LoginComponent, canActivate: [UnauthGuard] },
|
{ path: "login", component: LoginComponent, canActivate: [UnauthGuard] },
|
||||||
|
{
|
||||||
|
path: "login-with-device",
|
||||||
|
component: LoginWithDeviceComponent,
|
||||||
|
data: { titleId: "loginWithDevice" },
|
||||||
|
},
|
||||||
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
|
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
|
||||||
{
|
{
|
||||||
path: "register",
|
path: "register",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { LoginModule } from "./accounts/login/login.module";
|
||||||
import { TrialInitiationModule } from "./accounts/trial-initiation/trial-initiation.module";
|
import { TrialInitiationModule } from "./accounts/trial-initiation/trial-initiation.module";
|
||||||
import { OrganizationCreateModule } from "./organizations/create/organization-create.module";
|
import { OrganizationCreateModule } from "./organizations/create/organization-create.module";
|
||||||
import { OrganizationManageModule } from "./organizations/manage/organization-manage.module";
|
import { OrganizationManageModule } from "./organizations/manage/organization-manage.module";
|
||||||
|
@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
|
||||||
OrganizationManageModule,
|
OrganizationManageModule,
|
||||||
OrganizationUserModule,
|
OrganizationUserModule,
|
||||||
OrganizationCreateModule,
|
OrganizationCreateModule,
|
||||||
|
LoginModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
@ -25,6 +27,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
|
||||||
TrialInitiationModule,
|
TrialInitiationModule,
|
||||||
VaultFilterModule,
|
VaultFilterModule,
|
||||||
OrganizationBadgeModule,
|
OrganizationBadgeModule,
|
||||||
|
LoginModule,
|
||||||
],
|
],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { AcceptEmergencyComponent } from "../accounts/accept-emergency.component
|
||||||
import { AcceptOrganizationComponent } from "../accounts/accept-organization.component";
|
import { AcceptOrganizationComponent } from "../accounts/accept-organization.component";
|
||||||
import { HintComponent } from "../accounts/hint.component";
|
import { HintComponent } from "../accounts/hint.component";
|
||||||
import { LockComponent } from "../accounts/lock.component";
|
import { LockComponent } from "../accounts/lock.component";
|
||||||
import { LoginComponent } from "../accounts/login.component";
|
|
||||||
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
|
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
|
||||||
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
|
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
|
||||||
import { RegisterFormModule } from "../accounts/register-form/register-form.module";
|
import { RegisterFormModule } from "../accounts/register-form/register-form.module";
|
||||||
|
@ -210,7 +209,6 @@ import { SharedModule } from ".";
|
||||||
FrontendLayoutComponent,
|
FrontendLayoutComponent,
|
||||||
HintComponent,
|
HintComponent,
|
||||||
LockComponent,
|
LockComponent,
|
||||||
LoginComponent,
|
|
||||||
MasterPasswordPolicyComponent,
|
MasterPasswordPolicyComponent,
|
||||||
NavbarComponent,
|
NavbarComponent,
|
||||||
NestedCheckboxComponent,
|
NestedCheckboxComponent,
|
||||||
|
@ -355,7 +353,6 @@ import { SharedModule } from ".";
|
||||||
FrontendLayoutComponent,
|
FrontendLayoutComponent,
|
||||||
HintComponent,
|
HintComponent,
|
||||||
LockComponent,
|
LockComponent,
|
||||||
LoginComponent,
|
|
||||||
MasterPasswordPolicyComponent,
|
MasterPasswordPolicyComponent,
|
||||||
NavbarComponent,
|
NavbarComponent,
|
||||||
NestedCheckboxComponent,
|
NestedCheckboxComponent,
|
||||||
|
|
|
@ -569,15 +569,27 @@
|
||||||
"loginOrCreateNewAccount": {
|
"loginOrCreateNewAccount": {
|
||||||
"message": "Log in or create a new account to access your secure vault."
|
"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": {
|
"createAccount": {
|
||||||
"message": "Create Account"
|
"message": "Create Account"
|
||||||
},
|
},
|
||||||
|
"newAroundHere": {
|
||||||
|
"message": "New around here?"
|
||||||
|
},
|
||||||
"startTrial": {
|
"startTrial": {
|
||||||
"message": "Start Trial"
|
"message": "Start Trial"
|
||||||
},
|
},
|
||||||
"logIn": {
|
"logIn": {
|
||||||
"message": "Log In"
|
"message": "Log In"
|
||||||
},
|
},
|
||||||
|
"logInInitiated": {
|
||||||
|
"message": "Log in initiated"
|
||||||
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
"message": "Submit"
|
"message": "Submit"
|
||||||
},
|
},
|
||||||
|
@ -635,7 +647,7 @@
|
||||||
"confirmMasterPasswordRequired": {
|
"confirmMasterPasswordRequired": {
|
||||||
"message": "Master password retype is required."
|
"message": "Master password retype is required."
|
||||||
},
|
},
|
||||||
"masterPasswordMinLength": {
|
"masterPasswordMinlength": {
|
||||||
"message": "Master password must be at least 8 characters long."
|
"message": "Master password must be at least 8 characters long."
|
||||||
},
|
},
|
||||||
"masterPassDoesntMatch": {
|
"masterPassDoesntMatch": {
|
||||||
|
@ -705,6 +717,9 @@
|
||||||
"noOrganizationsList": {
|
"noOrganizationsList": {
|
||||||
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users."
|
"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": {
|
"versionNumber": {
|
||||||
"message": "Version $VERSION_NUMBER$",
|
"message": "Version $VERSION_NUMBER$",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -2532,6 +2547,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"viewAllLoginOptions": {
|
||||||
|
"message": "View all log in options"
|
||||||
|
},
|
||||||
"viewedItemId": {
|
"viewedItemId": {
|
||||||
"message": "Viewed item $ID$.",
|
"message": "Viewed item $ID$.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -3372,6 +3390,12 @@
|
||||||
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
|
"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."
|
"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": {
|
"dontAskFingerprintAgain": {
|
||||||
"message": "Never prompt to verify fingerprint phrases for invited users (Not recommended)",
|
"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."
|
"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": {
|
"reinviteSelected": {
|
||||||
"message": "Resend Invitations"
|
"message": "Resend Invitations"
|
||||||
},
|
},
|
||||||
|
"resendNotification": {
|
||||||
|
"message": "Resend notification"
|
||||||
|
},
|
||||||
"noSelectedUsersApplicable": {
|
"noSelectedUsersApplicable": {
|
||||||
"message": "This action is not applicable to any of the selected users."
|
"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 */
|
/* eslint-disable-next-line @typescript-eslint/ban-types */
|
||||||
export type Flags = {
|
export type Flags = {
|
||||||
showTrial?: boolean;
|
showTrial?: boolean;
|
||||||
|
showPasswordless?: boolean;
|
||||||
} & SharedFlags;
|
} & SharedFlags;
|
||||||
|
|
||||||
// required to avoid linting errors when there are no flags
|
// required to avoid linting errors when there are no flags
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Directive, Input, NgZone, OnInit } from "@angular/core";
|
import { Directive, NgZone, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { take } from "rxjs/operators";
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||||
|
import {
|
||||||
|
AllValidationErrors,
|
||||||
|
FormValidationErrorsService,
|
||||||
|
} from "@bitwarden/common/abstractions/formValidationErrors.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
|
@ -18,16 +23,19 @@ import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class LoginComponent extends CaptchaProtectedComponent implements OnInit {
|
export class LoginComponent extends CaptchaProtectedComponent implements OnInit {
|
||||||
@Input() email = "";
|
|
||||||
@Input() rememberEmail = true;
|
|
||||||
|
|
||||||
masterPassword = "";
|
|
||||||
showPassword = false;
|
showPassword = false;
|
||||||
formPromise: Promise<AuthResult>;
|
formPromise: Promise<AuthResult>;
|
||||||
onSuccessfulLogin: () => Promise<any>;
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||||
|
selfHosted = false;
|
||||||
|
|
||||||
|
formGroup = this.formBuilder.group({
|
||||||
|
email: ["", [Validators.required, Validators.email]],
|
||||||
|
masterPassword: ["", [Validators.required, Validators.minLength(8)]],
|
||||||
|
rememberEmail: [true],
|
||||||
|
});
|
||||||
|
|
||||||
protected twoFactorRoute = "2fa";
|
protected twoFactorRoute = "2fa";
|
||||||
protected successRoute = "vault";
|
protected successRoute = "vault";
|
||||||
|
@ -44,9 +52,12 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||||
protected passwordGenerationService: PasswordGenerationService,
|
protected passwordGenerationService: PasswordGenerationService,
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected ngZone: NgZone
|
protected ngZone: NgZone,
|
||||||
|
protected formBuilder: FormBuilder,
|
||||||
|
protected formValidationErrorService: FormValidationErrorsService
|
||||||
) {
|
) {
|
||||||
super(environmentService, i18nService, platformUtilsService);
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
get selfHostedDomain() {
|
get selfHostedDomain() {
|
||||||
|
@ -54,59 +65,53 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (this.email == null || this.email === "") {
|
let email = this.formGroup.get("email")?.value;
|
||||||
this.email = await this.stateService.getRememberedEmail();
|
if (email == null || email === "") {
|
||||||
if (this.email == null) {
|
email = await this.stateService.getRememberedEmail();
|
||||||
this.email = "";
|
this.formGroup.get("email")?.setValue(email);
|
||||||
|
|
||||||
|
if (email == null) {
|
||||||
|
this.formGroup.get("email")?.setValue("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.alwaysRememberEmail) {
|
if (!this.alwaysRememberEmail) {
|
||||||
this.rememberEmail = (await this.stateService.getRememberedEmail()) != null;
|
const rememberEmail = (await this.stateService.getRememberedEmail()) != null;
|
||||||
}
|
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
|
||||||
if (Utils.isBrowser && !Utils.isNode) {
|
|
||||||
this.focusInput();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit(showToast = true) {
|
||||||
|
const email = this.formGroup.get("email")?.value;
|
||||||
|
const masterPassword = this.formGroup.get("masterPassword")?.value;
|
||||||
|
const rememberEmail = this.formGroup.get("rememberEmail")?.value;
|
||||||
|
|
||||||
await this.setupCaptcha();
|
await this.setupCaptcha();
|
||||||
|
|
||||||
if (this.email == null || this.email === "") {
|
this.formGroup.markAllAsTouched();
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
//web
|
||||||
this.i18nService.t("errorOccurred"),
|
if (this.formGroup.invalid && !showToast) {
|
||||||
this.i18nService.t("emailRequired")
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.email.indexOf("@") === -1) {
|
|
||||||
this.platformUtilsService.showToast(
|
//desktop, browser; This should be removed once all clients use reactive forms
|
||||||
"error",
|
if (this.formGroup.invalid && showToast) {
|
||||||
this.i18nService.t("errorOccurred"),
|
const errorText = this.getErrorToastMessage();
|
||||||
this.i18nService.t("invalidEmail")
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.masterPassword == null || this.masterPassword === "") {
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("masterPasswordRequired")
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentials = new PasswordLogInCredentials(
|
const credentials = new PasswordLogInCredentials(
|
||||||
this.email,
|
email,
|
||||||
this.masterPassword,
|
masterPassword,
|
||||||
this.captchaToken,
|
this.captchaToken,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
this.formPromise = this.authService.logIn(credentials);
|
this.formPromise = this.authService.logIn(credentials);
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
if (this.rememberEmail || this.alwaysRememberEmail) {
|
if (rememberEmail || this.alwaysRememberEmail) {
|
||||||
await this.stateService.setRememberedEmail(this.email);
|
await this.stateService.setRememberedEmail(email);
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setRememberedEmail(null);
|
await this.stateService.setRememberedEmail(null);
|
||||||
}
|
}
|
||||||
|
@ -188,9 +193,30 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getErrorToastMessage() {
|
||||||
|
const error: AllValidationErrors = this.formValidationErrorService
|
||||||
|
.getFormValidationErrors(this.formGroup.controls)
|
||||||
|
.shift();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
switch (error.errorName) {
|
||||||
|
case "email":
|
||||||
|
return this.i18nService.t("invalidEmail");
|
||||||
|
default:
|
||||||
|
return this.i18nService.t(this.errorTag(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private errorTag(error: AllValidationErrors): string {
|
||||||
|
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
|
||||||
|
return `${error.controlName}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
protected focusInput() {
|
protected focusInput() {
|
||||||
document
|
const email = this.formGroup.get("email")?.value;
|
||||||
.getElementById(this.email == null || this.email === "" ? "email" : "masterPassword")
|
document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus();
|
||||||
.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { AbstractThemingService } from "@bitwarden/angular/services/theming/them
|
||||||
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
|
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
|
||||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/abstractions/account/account-api.service.abstraction";
|
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/abstractions/account/account-api.service.abstraction";
|
||||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/abstractions/account/account.service.abstraction";
|
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/abstractions/account/account.service.abstraction";
|
||||||
|
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
|
||||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||||
|
@ -62,6 +63,7 @@ import { Account } from "@bitwarden/common/models/domain/account";
|
||||||
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
||||||
import { AccountApiService } from "@bitwarden/common/services/account/account-api.service";
|
import { AccountApiService } from "@bitwarden/common/services/account/account-api.service";
|
||||||
import { AccountService } from "@bitwarden/common/services/account/account.service";
|
import { AccountService } from "@bitwarden/common/services/account/account.service";
|
||||||
|
import { AnonymousHubService } from "@bitwarden/common/services/anonymousHub.service";
|
||||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||||
import { AppIdService } from "@bitwarden/common/services/appId.service";
|
import { AppIdService } from "@bitwarden/common/services/appId.service";
|
||||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||||
|
@ -544,6 +546,11 @@ import { ValidationService } from "./validation.service";
|
||||||
useClass: ConfigApiService,
|
useClass: ConfigApiService,
|
||||||
deps: [ApiServiceAbstraction],
|
deps: [ApiServiceAbstraction],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AnonymousHubServiceAbstraction,
|
||||||
|
useClass: AnonymousHubService,
|
||||||
|
deps: [EnvironmentServiceAbstraction, AuthServiceAbstraction, LogService],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export abstract class AnonymousHubService {
|
||||||
|
createHubConnection: (token: string) => void;
|
||||||
|
stopHubConnection: () => void;
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat
|
||||||
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
|
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
|
||||||
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
|
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
|
||||||
import { PasswordRequest } from "../models/request/passwordRequest";
|
import { PasswordRequest } from "../models/request/passwordRequest";
|
||||||
|
import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest";
|
||||||
import { PaymentRequest } from "../models/request/paymentRequest";
|
import { PaymentRequest } from "../models/request/paymentRequest";
|
||||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
import { PreloginRequest } from "../models/request/preloginRequest";
|
||||||
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
|
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
|
||||||
|
@ -84,6 +85,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
||||||
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||||
|
import { AuthRequestResponse } from "../models/response/authRequestResponse";
|
||||||
import { RegisterResponse } from "../models/response/authentication/registerResponse";
|
import { RegisterResponse } from "../models/response/authentication/registerResponse";
|
||||||
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
|
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
|
||||||
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
|
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
|
||||||
|
@ -210,6 +212,9 @@ export abstract class ApiService {
|
||||||
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||||
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
||||||
postConvertToKeyConnector: () => Promise<void>;
|
postConvertToKeyConnector: () => Promise<void>;
|
||||||
|
//passwordless
|
||||||
|
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||||
|
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||||
|
|
||||||
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
|
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
|
||||||
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
|
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "../enums/authenticationStatus";
|
import { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||||
import { AuthResult } from "../models/domain/authResult";
|
import { AuthResult } from "../models/domain/authResult";
|
||||||
import {
|
import {
|
||||||
ApiLogInCredentials,
|
ApiLogInCredentials,
|
||||||
PasswordLogInCredentials,
|
PasswordLogInCredentials,
|
||||||
SsoLogInCredentials,
|
SsoLogInCredentials,
|
||||||
|
PasswordlessLogInCredentials,
|
||||||
} from "../models/domain/logInCredentials";
|
} from "../models/domain/logInCredentials";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
||||||
|
import { AuthRequestPushNotification } from "../models/response/notificationResponse";
|
||||||
|
|
||||||
export abstract class AuthService {
|
export abstract class AuthService {
|
||||||
masterPasswordHash: string;
|
masterPasswordHash: string;
|
||||||
email: string;
|
email: string;
|
||||||
logIn: (
|
logIn: (
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
credentials:
|
||||||
|
| ApiLogInCredentials
|
||||||
|
| PasswordLogInCredentials
|
||||||
|
| SsoLogInCredentials
|
||||||
|
| PasswordlessLogInCredentials
|
||||||
) => Promise<AuthResult>;
|
) => Promise<AuthResult>;
|
||||||
logInTwoFactor: (
|
logInTwoFactor: (
|
||||||
twoFactor: TokenRequestTwoFactor,
|
twoFactor: TokenRequestTwoFactor,
|
||||||
|
@ -24,4 +32,7 @@ export abstract class AuthService {
|
||||||
authingWithSso: () => boolean;
|
authingWithSso: () => boolean;
|
||||||
authingWithPassword: () => boolean;
|
authingWithPassword: () => boolean;
|
||||||
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
|
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
|
||||||
|
authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise<any>;
|
||||||
|
|
||||||
|
getPushNotifcationObs$: () => Observable<any>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum AuthRequestType {
|
||||||
|
AuthenticateAndUnlock = 0,
|
||||||
|
Unlock = 1,
|
||||||
|
}
|
|
@ -2,4 +2,5 @@ export enum AuthenticationType {
|
||||||
Password = 0,
|
Password = 0,
|
||||||
Sso = 1,
|
Sso = 1,
|
||||||
Api = 2,
|
Api = 2,
|
||||||
|
Passwordless = 3,
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,7 @@ export enum NotificationType {
|
||||||
SyncSendCreate = 12,
|
SyncSendCreate = 12,
|
||||||
SyncSendUpdate = 13,
|
SyncSendUpdate = 13,
|
||||||
SyncSendDelete = 14,
|
SyncSendDelete = 14,
|
||||||
|
|
||||||
|
AuthRequest = 15,
|
||||||
|
AuthRequestResponse = 16,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
ApiLogInCredentials,
|
ApiLogInCredentials,
|
||||||
PasswordLogInCredentials,
|
PasswordLogInCredentials,
|
||||||
SsoLogInCredentials,
|
SsoLogInCredentials,
|
||||||
|
PasswordlessLogInCredentials,
|
||||||
} from "../../models/domain/logInCredentials";
|
} from "../../models/domain/logInCredentials";
|
||||||
import { DeviceRequest } from "../../models/request/deviceRequest";
|
import { DeviceRequest } from "../../models/request/deviceRequest";
|
||||||
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
|
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
|
||||||
|
@ -42,7 +43,11 @@ export abstract class LogInStrategy {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
abstract logIn(
|
abstract logIn(
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
credentials:
|
||||||
|
| ApiLogInCredentials
|
||||||
|
| PasswordLogInCredentials
|
||||||
|
| SsoLogInCredentials
|
||||||
|
| PasswordlessLogInCredentials
|
||||||
): Promise<AuthResult>;
|
): Promise<AuthResult>;
|
||||||
|
|
||||||
async logInTwoFactor(
|
async logInTwoFactor(
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
|
import { AppIdService } from "../../abstractions/appId.service";
|
||||||
|
import { AuthService } from "../../abstractions/auth.service";
|
||||||
|
import { CryptoService } from "../../abstractions/crypto.service";
|
||||||
|
import { LogService } from "../../abstractions/log.service";
|
||||||
|
import { MessagingService } from "../../abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
import { TokenService } from "../../abstractions/token.service";
|
||||||
|
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||||
|
import { AuthResult } from "../../models/domain/authResult";
|
||||||
|
import { PasswordlessLogInCredentials } from "../../models/domain/logInCredentials";
|
||||||
|
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
||||||
|
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
||||||
|
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
|
||||||
|
|
||||||
|
import { LogInStrategy } from "./logIn.strategy";
|
||||||
|
|
||||||
|
export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||||
|
get email() {
|
||||||
|
return this.tokenRequest.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get masterPasswordHash() {
|
||||||
|
return this.tokenRequest.masterPasswordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRequest: PasswordTokenRequest;
|
||||||
|
|
||||||
|
private localHashedPassword: string;
|
||||||
|
private key: SymmetricCryptoKey;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
apiService: ApiService,
|
||||||
|
tokenService: TokenService,
|
||||||
|
appIdService: AppIdService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
messagingService: MessagingService,
|
||||||
|
logService: LogService,
|
||||||
|
stateService: StateService,
|
||||||
|
twoFactorService: TwoFactorService,
|
||||||
|
private authService: AuthService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
cryptoService,
|
||||||
|
apiService,
|
||||||
|
tokenService,
|
||||||
|
appIdService,
|
||||||
|
platformUtilsService,
|
||||||
|
messagingService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
twoFactorService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSuccessfulLogin() {
|
||||||
|
await this.cryptoService.setKey(this.key);
|
||||||
|
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
async logInTwoFactor(
|
||||||
|
twoFactor: TokenRequestTwoFactor,
|
||||||
|
captchaResponse: string
|
||||||
|
): Promise<AuthResult> {
|
||||||
|
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||||
|
return super.logInTwoFactor(twoFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async logIn(credentials: PasswordlessLogInCredentials) {
|
||||||
|
this.localHashedPassword = credentials.localPasswordHash;
|
||||||
|
this.key = credentials.decKey;
|
||||||
|
|
||||||
|
this.tokenRequest = new PasswordTokenRequest(
|
||||||
|
credentials.email,
|
||||||
|
credentials.accessCode,
|
||||||
|
null,
|
||||||
|
await this.buildTwoFactor(credentials.twoFactor),
|
||||||
|
await this.buildDeviceRequest()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.tokenRequest.setPasswordlessAccessCode(credentials.authRequestId);
|
||||||
|
return this.startLogIn();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
import { AuthenticationType } from "../../enums/authenticationType";
|
import { AuthenticationType } from "../../enums/authenticationType";
|
||||||
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
|
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
|
||||||
|
|
||||||
|
@ -29,3 +31,16 @@ export class ApiLogInCredentials {
|
||||||
|
|
||||||
constructor(public clientId: string, public clientSecret: string) {}
|
constructor(public clientId: string, public clientSecret: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PasswordlessLogInCredentials {
|
||||||
|
readonly type = AuthenticationType.Passwordless;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public email: string,
|
||||||
|
public accessCode: string,
|
||||||
|
public authRequestId: string,
|
||||||
|
public decKey: SymmetricCryptoKey,
|
||||||
|
public localPasswordHash: string,
|
||||||
|
public twoFactor?: TokenRequestTwoFactor
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
|
||||||
|
|
||||||
export abstract class TokenRequest {
|
export abstract class TokenRequest {
|
||||||
protected device?: DeviceRequest;
|
protected device?: DeviceRequest;
|
||||||
|
protected passwordlessAuthRequest: string;
|
||||||
|
|
||||||
constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) {
|
constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) {
|
||||||
this.device = device != null ? device : null;
|
this.device = device != null ? device : null;
|
||||||
|
@ -18,6 +19,10 @@ export abstract class TokenRequest {
|
||||||
this.twoFactor = twoFactor;
|
this.twoFactor = twoFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPasswordlessAccessCode(accessCode: string) {
|
||||||
|
this.passwordlessAuthRequest = accessCode;
|
||||||
|
}
|
||||||
|
|
||||||
protected toIdentityToken(clientId: string) {
|
protected toIdentityToken(clientId: string) {
|
||||||
const obj: any = {
|
const obj: any = {
|
||||||
scope: "api offline_access",
|
scope: "api offline_access",
|
||||||
|
@ -32,6 +37,11 @@ export abstract class TokenRequest {
|
||||||
// obj.devicePushToken = this.device.pushToken;
|
// obj.devicePushToken = this.device.pushToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//passswordless login
|
||||||
|
if (this.passwordlessAuthRequest) {
|
||||||
|
obj.authRequest = this.passwordlessAuthRequest;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.twoFactor.token && this.twoFactor.provider != null) {
|
if (this.twoFactor.token && this.twoFactor.provider != null) {
|
||||||
obj.twoFactorToken = this.twoFactor.token;
|
obj.twoFactorToken = this.twoFactor.token;
|
||||||
obj.twoFactorProvider = this.twoFactor.provider;
|
obj.twoFactorProvider = this.twoFactor.provider;
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { AuthRequestType } from "../../enums/authRequestType";
|
||||||
|
|
||||||
|
export class PasswordlessCreateAuthRequest {
|
||||||
|
constructor(
|
||||||
|
readonly email: string,
|
||||||
|
readonly deviceIdentifier: string,
|
||||||
|
readonly publicKey: string,
|
||||||
|
readonly type: AuthRequestType,
|
||||||
|
readonly accessCode: string,
|
||||||
|
readonly fingerprintPhrase: string
|
||||||
|
) {}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums/deviceType";
|
||||||
|
|
||||||
|
import { BaseResponse } from "./baseResponse";
|
||||||
|
|
||||||
|
export class AuthRequestResponse extends BaseResponse {
|
||||||
|
id: string;
|
||||||
|
publicKey: string;
|
||||||
|
requestDeviceType: DeviceType;
|
||||||
|
requestIpAddress: string;
|
||||||
|
key: string;
|
||||||
|
masterPasswordHash: string;
|
||||||
|
creationDate: string;
|
||||||
|
requestApproved: boolean;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.id = this.getResponseProperty("Id");
|
||||||
|
this.publicKey = this.getResponseProperty("PublicKey");
|
||||||
|
this.requestDeviceType = this.getResponseProperty("RequestDeviceType");
|
||||||
|
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
|
||||||
|
this.key = this.getResponseProperty("Key");
|
||||||
|
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
||||||
|
this.creationDate = this.getResponseProperty("CreationDate");
|
||||||
|
this.requestApproved = this.getResponseProperty("RequestApproved");
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,10 @@ export class NotificationResponse extends BaseResponse {
|
||||||
case NotificationType.SyncSendDelete:
|
case NotificationType.SyncSendDelete:
|
||||||
this.payload = new SyncSendNotification(payload);
|
this.payload = new SyncSendNotification(payload);
|
||||||
break;
|
break;
|
||||||
|
case NotificationType.AuthRequest:
|
||||||
|
case NotificationType.AuthRequestResponse:
|
||||||
|
this.payload = new AuthRequestPushNotification(payload);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -96,3 +100,14 @@ export class SyncSendNotification extends BaseResponse {
|
||||||
this.revisionDate = new Date(this.getResponseProperty("RevisionDate"));
|
this.revisionDate = new Date(this.getResponseProperty("RevisionDate"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AuthRequestPushNotification extends BaseResponse {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.id = this.getResponseProperty("Id");
|
||||||
|
this.userId = this.getResponseProperty("UserId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import {
|
||||||
|
HttpTransportType,
|
||||||
|
HubConnection,
|
||||||
|
HubConnectionBuilder,
|
||||||
|
IHubProtocol,
|
||||||
|
} from "@microsoft/signalr";
|
||||||
|
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
||||||
|
|
||||||
|
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "../abstractions/anonymousHub.service";
|
||||||
|
import { AuthService } from "../abstractions/auth.service";
|
||||||
|
import { EnvironmentService } from "../abstractions/environment.service";
|
||||||
|
import { LogService } from "../abstractions/log.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthRequestPushNotification,
|
||||||
|
NotificationResponse,
|
||||||
|
} from "./../models/response/notificationResponse";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnonymousHubService implements AnonymousHubServiceAbstraction {
|
||||||
|
private anonHubConnection: HubConnection;
|
||||||
|
private url: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private logService: LogService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createHubConnection(token: string) {
|
||||||
|
this.url = this.environmentService.getNotificationsUrl();
|
||||||
|
|
||||||
|
this.anonHubConnection = new HubConnectionBuilder()
|
||||||
|
.withUrl(this.url + "/anonymousHub?Token=" + token, {
|
||||||
|
skipNegotiation: true,
|
||||||
|
transport: HttpTransportType.WebSockets,
|
||||||
|
})
|
||||||
|
.withHubProtocol(new MessagePackHubProtocol() as IHubProtocol)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.anonHubConnection.start().catch((error) => this.logService.error(error));
|
||||||
|
|
||||||
|
this.anonHubConnection.on("AuthRequestResponseRecieved", (data: any) => {
|
||||||
|
this.ProcessNotification(new NotificationResponse(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stopHubConnection() {
|
||||||
|
if (this.anonHubConnection) {
|
||||||
|
this.anonHubConnection.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ProcessNotification(notification: NotificationResponse) {
|
||||||
|
await this.authService.authResponsePushNotifiction(
|
||||||
|
notification.payload as AuthRequestPushNotification
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,6 +54,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat
|
||||||
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
|
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
|
||||||
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
|
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
|
||||||
import { PasswordRequest } from "../models/request/passwordRequest";
|
import { PasswordRequest } from "../models/request/passwordRequest";
|
||||||
|
import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest";
|
||||||
import { PaymentRequest } from "../models/request/paymentRequest";
|
import { PaymentRequest } from "../models/request/paymentRequest";
|
||||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
import { PreloginRequest } from "../models/request/preloginRequest";
|
||||||
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
|
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
|
||||||
|
@ -92,6 +93,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
||||||
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||||
|
import { AuthRequestResponse } from "../models/response/authRequestResponse";
|
||||||
import { RegisterResponse } from "../models/response/authentication/registerResponse";
|
import { RegisterResponse } from "../models/response/authentication/registerResponse";
|
||||||
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
|
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
|
||||||
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
|
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
|
||||||
|
@ -265,6 +267,17 @@ export class ApiService implements ApiServiceAbstraction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async postAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
|
||||||
|
const r = await this.send("POST", "/auth-requests/", request, false, true);
|
||||||
|
return new AuthRequestResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthResponse(id: string, accessCode: string): Promise<AuthRequestResponse> {
|
||||||
|
const path = `/auth-requests/${id}/response?code=${accessCode}`;
|
||||||
|
const r = await this.send("GET", path, null, false, true);
|
||||||
|
return new AuthRequestResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
// Account APIs
|
// Account APIs
|
||||||
|
|
||||||
async getProfile(): Promise<ProfileResponse> {
|
async getProfile(): Promise<ProfileResponse> {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Observable, Subject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../abstractions/api.service";
|
import { ApiService } from "../abstractions/api.service";
|
||||||
import { AppIdService } from "../abstractions/appId.service";
|
import { AppIdService } from "../abstractions/appId.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||||
|
@ -17,17 +19,20 @@ import { KdfType } from "../enums/kdfType";
|
||||||
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
import { KeySuffixOptions } from "../enums/keySuffixOptions";
|
||||||
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
||||||
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
||||||
|
import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy";
|
||||||
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
||||||
import { AuthResult } from "../models/domain/authResult";
|
import { AuthResult } from "../models/domain/authResult";
|
||||||
import {
|
import {
|
||||||
ApiLogInCredentials,
|
ApiLogInCredentials,
|
||||||
PasswordLogInCredentials,
|
PasswordLogInCredentials,
|
||||||
SsoLogInCredentials,
|
SsoLogInCredentials,
|
||||||
|
PasswordlessLogInCredentials,
|
||||||
} from "../models/domain/logInCredentials";
|
} from "../models/domain/logInCredentials";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
||||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
import { PreloginRequest } from "../models/request/preloginRequest";
|
||||||
import { ErrorResponse } from "../models/response/errorResponse";
|
import { ErrorResponse } from "../models/response/errorResponse";
|
||||||
|
import { AuthRequestPushNotification } from "../models/response/notificationResponse";
|
||||||
|
|
||||||
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
|
@ -42,9 +47,15 @@ export class AuthService implements AuthServiceAbstraction {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
private logInStrategy:
|
||||||
|
| ApiLogInStrategy
|
||||||
|
| PasswordLogInStrategy
|
||||||
|
| SsoLogInStrategy
|
||||||
|
| PasswordlessLogInStrategy;
|
||||||
private sessionTimeout: any;
|
private sessionTimeout: any;
|
||||||
|
|
||||||
|
private pushNotificationSubject = new Subject<string>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
|
@ -61,52 +72,78 @@ export class AuthService implements AuthServiceAbstraction {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async logIn(
|
async logIn(
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
credentials:
|
||||||
|
| ApiLogInCredentials
|
||||||
|
| PasswordLogInCredentials
|
||||||
|
| SsoLogInCredentials
|
||||||
|
| PasswordlessLogInCredentials
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.clearState();
|
this.clearState();
|
||||||
|
|
||||||
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
let strategy:
|
||||||
|
| ApiLogInStrategy
|
||||||
|
| PasswordLogInStrategy
|
||||||
|
| SsoLogInStrategy
|
||||||
|
| PasswordlessLogInStrategy;
|
||||||
|
|
||||||
if (credentials.type === AuthenticationType.Password) {
|
switch (credentials.type) {
|
||||||
strategy = new PasswordLogInStrategy(
|
case AuthenticationType.Password:
|
||||||
this.cryptoService,
|
strategy = new PasswordLogInStrategy(
|
||||||
this.apiService,
|
this.cryptoService,
|
||||||
this.tokenService,
|
this.apiService,
|
||||||
this.appIdService,
|
this.tokenService,
|
||||||
this.platformUtilsService,
|
this.appIdService,
|
||||||
this.messagingService,
|
this.platformUtilsService,
|
||||||
this.logService,
|
this.messagingService,
|
||||||
this.stateService,
|
this.logService,
|
||||||
this.twoFactorService,
|
this.stateService,
|
||||||
this
|
this.twoFactorService,
|
||||||
);
|
this
|
||||||
} else if (credentials.type === AuthenticationType.Sso) {
|
);
|
||||||
strategy = new SsoLogInStrategy(
|
break;
|
||||||
this.cryptoService,
|
case AuthenticationType.Sso:
|
||||||
this.apiService,
|
strategy = new SsoLogInStrategy(
|
||||||
this.tokenService,
|
this.cryptoService,
|
||||||
this.appIdService,
|
this.apiService,
|
||||||
this.platformUtilsService,
|
this.tokenService,
|
||||||
this.messagingService,
|
this.appIdService,
|
||||||
this.logService,
|
this.platformUtilsService,
|
||||||
this.stateService,
|
this.messagingService,
|
||||||
this.twoFactorService,
|
this.logService,
|
||||||
this.keyConnectorService
|
this.stateService,
|
||||||
);
|
this.twoFactorService,
|
||||||
} else if (credentials.type === AuthenticationType.Api) {
|
this.keyConnectorService
|
||||||
strategy = new ApiLogInStrategy(
|
);
|
||||||
this.cryptoService,
|
break;
|
||||||
this.apiService,
|
case AuthenticationType.Api:
|
||||||
this.tokenService,
|
strategy = new ApiLogInStrategy(
|
||||||
this.appIdService,
|
this.cryptoService,
|
||||||
this.platformUtilsService,
|
this.apiService,
|
||||||
this.messagingService,
|
this.tokenService,
|
||||||
this.logService,
|
this.appIdService,
|
||||||
this.stateService,
|
this.platformUtilsService,
|
||||||
this.twoFactorService,
|
this.messagingService,
|
||||||
this.environmentService,
|
this.logService,
|
||||||
this.keyConnectorService
|
this.stateService,
|
||||||
);
|
this.twoFactorService,
|
||||||
|
this.environmentService,
|
||||||
|
this.keyConnectorService
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case AuthenticationType.Passwordless:
|
||||||
|
strategy = new PasswordlessLogInStrategy(
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService,
|
||||||
|
this.twoFactorService,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await strategy.logIn(credentials as any);
|
const result = await strategy.logIn(credentials as any);
|
||||||
|
@ -202,7 +239,21 @@ export class AuthService implements AuthServiceAbstraction {
|
||||||
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
|
async authResponsePushNotifiction(notification: AuthRequestPushNotification): Promise<any> {
|
||||||
|
this.pushNotificationSubject.next(notification.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPushNotifcationObs$(): Observable<any> {
|
||||||
|
return this.pushNotificationSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveState(
|
||||||
|
strategy:
|
||||||
|
| ApiLogInStrategy
|
||||||
|
| PasswordLogInStrategy
|
||||||
|
| SsoLogInStrategy
|
||||||
|
| PasswordlessLogInStrategy
|
||||||
|
) {
|
||||||
this.logInStrategy = strategy;
|
this.logInStrategy = strategy;
|
||||||
this.startSessionTimeout();
|
this.startSessionTimeout();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue