@@ -40,10 +34,8 @@
diff --git a/apps/desktop/src/app/accounts/login.component.ts b/apps/desktop/src/app/accounts/login.component.ts
index 959c8a4565..33eefbd57e 100644
--- a/apps/desktop/src/app/accounts/login.component.ts
+++ b/apps/desktop/src/app/accounts/login.component.ts
@@ -1,4 +1,5 @@
import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
+import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
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 { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
+import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
@@ -47,7 +49,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
private broadcasterService: BroadcasterService,
ngZone: NgZone,
private messagingService: MessagingService,
- logService: LogService
+ logService: LogService,
+ formBuilder: FormBuilder,
+ formValidationErrorService: FormValidationErrorsService
) {
super(
authService,
@@ -59,7 +63,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
passwordGenerationService,
cryptoFunctionService,
logService,
- ngZone
+ ngZone,
+ formBuilder,
+ formValidationErrorService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
diff --git a/apps/web/config/base.json b/apps/web/config/base.json
index cab6fbe950..8eb8a31133 100644
--- a/apps/web/config/base.json
+++ b/apps/web/config/base.json
@@ -10,5 +10,7 @@
"port": 8080,
"allowedHosts": "auto"
},
- "flags": {}
+ "flags": {
+ "showPasswordless": false
+ }
}
diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json
index 96d692f7e8..5bd5e6b060 100644
--- a/apps/web/config/cloud.json
+++ b/apps/web/config/cloud.json
@@ -16,6 +16,7 @@
"proxyEvents": "https://events.bitwarden.com"
},
"flags": {
- "showTrial": true
+ "showTrial": true,
+ "showPasswordless": false
}
}
diff --git a/apps/web/config/development.json b/apps/web/config/development.json
index e3048db7a2..f460a1659a 100644
--- a/apps/web/config/development.json
+++ b/apps/web/config/development.json
@@ -10,6 +10,7 @@
"proxyNotifications": "http://localhost:61840"
},
"flags": {
- "showTrial": true
+ "showTrial": true,
+ "showPasswordless": true
}
}
diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json
index 4371ea1ff9..a0d1b0e88c 100644
--- a/apps/web/config/qa.json
+++ b/apps/web/config/qa.json
@@ -10,6 +10,7 @@
"proxyEvents": "https://events.qa.bitwarden.pw"
},
"flags": {
- "showTrial": true
+ "showTrial": true,
+ "showPasswordless": true
}
}
diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json
index 3ba61fda59..b37a922604 100644
--- a/apps/web/config/selfhosted.json
+++ b/apps/web/config/selfhosted.json
@@ -7,6 +7,7 @@
"port": 8081
},
"flags": {
- "showTrial": false
+ "showTrial": false,
+ "showPasswordless": false
}
}
diff --git a/apps/web/src/app/accounts/login.component.html b/apps/web/src/app/accounts/login.component.html
deleted file mode 100644
index e0c4ef68db..0000000000
--- a/apps/web/src/app/accounts/login.component.html
+++ /dev/null
@@ -1,102 +0,0 @@
-
diff --git a/apps/web/src/app/accounts/login/login-with-device.component.html b/apps/web/src/app/accounts/login/login-with-device.component.html
new file mode 100644
index 0000000000..3105a639ad
--- /dev/null
+++ b/apps/web/src/app/accounts/login/login-with-device.component.html
@@ -0,0 +1,44 @@
+
+
+
+
+ {{ "loginOrCreateNewAccount" | i18n }}
+
+
+
+
{{ "logInInitiated" | i18n }}
+
+
+
{{ "notificationSentDevice" | i18n }}
+
+
+ {{ "fingerprintMatchInfo" | i18n }}
+
+
+
+
+
{{ "fingerprintPhraseHeader" | i18n }}
+
+ {{ passwordlessRequest?.fingerprintPhrase }}
+
+
+
+
+
+
+
+
+ {{ "loginWithDevciceEnabledInfo" | i18n }}
+
{{ "viewAllLoginOptions" | i18n }}
+
+
+
+
diff --git a/apps/web/src/app/accounts/login/login-with-device.component.ts b/apps/web/src/app/accounts/login/login-with-device.component.ts
new file mode 100644
index 0000000000..4c6f6268df
--- /dev/null
+++ b/apps/web/src/app/accounts/login/login-with-device.component.ts
@@ -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
();
+ email: string;
+ showResendNotification = false;
+ passwordlessRequest: PasswordlessCreateAuthRequest;
+ onSuccessfulLoginTwoFactorNavigate: () => Promise;
+ onSuccessfulLogin: () => Promise;
+ onSuccessfulLoginNavigate: () => Promise;
+
+ 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 {
+ 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
+ );
+ }
+}
diff --git a/apps/web/src/app/accounts/login/login.component.html b/apps/web/src/app/accounts/login/login.component.html
new file mode 100644
index 0000000000..7df9777f39
--- /dev/null
+++ b/apps/web/src/app/accounts/login/login.component.html
@@ -0,0 +1,121 @@
+
diff --git a/apps/web/src/app/accounts/login.component.ts b/apps/web/src/app/accounts/login/login.component.ts
similarity index 80%
rename from apps/web/src/app/accounts/login.component.ts
rename to apps/web/src/app/accounts/login/login.component.ts
index 6a13ff3233..8664ae4a57 100644
--- a/apps/web/src/app/accounts/login.component.ts
+++ b/apps/web/src/app/accounts/login/login.component.ts
@@ -1,4 +1,5 @@
import { Component, NgZone } from "@angular/core";
+import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
@@ -7,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
+import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
@@ -20,7 +22,9 @@ import { Policy } from "@bitwarden/common/models/domain/policy";
import { ListResponse } from "@bitwarden/common/models/response/listResponse";
import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse";
-import { RouterService, StateService } from "../core";
+import { flagEnabled } from "src/utils/flags";
+
+import { RouterService, StateService } from "../../core";
@Component({
selector: "app-login",
@@ -31,6 +35,7 @@ export class LoginComponent extends BaseLoginComponent {
showResetPasswordAutoEnrollWarning = false;
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
policies: ListResponse;
+ showPasswordless = false;
constructor(
authService: AuthService,
@@ -48,7 +53,9 @@ export class LoginComponent extends BaseLoginComponent {
ngZone: NgZone,
protected stateService: StateService,
private messagingService: MessagingService,
- private routerService: RouterService
+ private routerService: RouterService,
+ formBuilder: FormBuilder,
+ formValidationErrorService: FormValidationErrorsService
) {
super(
authService,
@@ -60,19 +67,22 @@ export class LoginComponent extends BaseLoginComponent {
passwordGenerationService,
cryptoFunctionService,
logService,
- ngZone
+ ngZone,
+ formBuilder,
+ formValidationErrorService
);
this.onSuccessfulLogin = async () => {
this.messagingService.send("setFullWidth");
};
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
+ this.showPasswordless = flagEnabled("showPasswordless");
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
- this.email = qParams.email;
+ this.formGroup.get("email")?.setValue(qParams.email);
}
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
@@ -91,7 +101,8 @@ export class LoginComponent extends BaseLoginComponent {
this.routerService.setPreviousUrl(route.toString());
}
await super.ngOnInit();
- this.rememberEmail = await this.stateService.getRememberEmail();
+ const rememberEmail = await this.stateService.getRememberEmail();
+ this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
});
const invite = await this.stateService.getOrganizationInvitation();
@@ -125,10 +136,12 @@ export class LoginComponent extends BaseLoginComponent {
}
async goAfterLogIn() {
+ const masterPassword = this.formGroup.get("masterPassword")?.value;
+
// Check master password against policy
if (this.enforcedPasswordPolicyOptions != null) {
const strengthResult = this.passwordGenerationService.passwordStrength(
- this.masterPassword,
+ masterPassword,
this.getPasswordStrengthUserInput()
);
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
@@ -137,7 +150,7 @@ export class LoginComponent extends BaseLoginComponent {
if (
!this.policyService.evaluateMasterPassword(
masterPasswordScore,
- this.masterPassword,
+ masterPassword,
this.enforcedPasswordPolicyOptions
)
) {
@@ -158,19 +171,34 @@ export class LoginComponent extends BaseLoginComponent {
}
async submit() {
- await this.stateService.setRememberEmail(this.rememberEmail);
- if (!this.rememberEmail) {
+ const rememberEmail = this.formGroup.get("rememberEmail")?.value;
+
+ await this.stateService.setRememberEmail(rememberEmail);
+ if (!rememberEmail) {
await this.stateService.setRememberedEmail(null);
}
- await super.submit();
+ await super.submit(false);
+ }
+
+ async startPasswordlessLogin() {
+ this.formGroup.get("masterPassword")?.clearValidators();
+ this.formGroup.get("masterPassword")?.updateValueAndValidity();
+
+ if (!this.formGroup.valid) {
+ return;
+ }
+
+ const email = this.formGroup.get("email").value;
+ this.router.navigate(["/login-with-device"], { state: { email: email } });
}
private getPasswordStrengthUserInput() {
+ const email = this.formGroup.get("email")?.value;
let userInput: string[] = [];
- const atPosition = this.email.indexOf("@");
+ const atPosition = email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
- this.email
+ email
.substr(0, atPosition)
.trim()
.toLowerCase()
diff --git a/apps/web/src/app/accounts/login/login.module.ts b/apps/web/src/app/accounts/login/login.module.ts
new file mode 100644
index 0000000000..9ab8dfb3a1
--- /dev/null
+++ b/apps/web/src/app/accounts/login/login.module.ts
@@ -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 {}
diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts
index 61daf7c82c..6e1c4e569e 100644
--- a/apps/web/src/app/oss-routing.module.ts
+++ b/apps/web/src/app/oss-routing.module.ts
@@ -11,7 +11,8 @@ import { AcceptEmergencyComponent } from "./accounts/accept-emergency.component"
import { AcceptOrganizationComponent } from "./accounts/accept-organization.component";
import { HintComponent } from "./accounts/hint.component";
import { LockComponent } from "./accounts/lock.component";
-import { LoginComponent } from "./accounts/login.component";
+import { LoginWithDeviceComponent } from "./accounts/login/login-with-device.component";
+import { LoginComponent } from "./accounts/login/login.component";
import { RecoverDeleteComponent } from "./accounts/recover-delete.component";
import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component";
import { RegisterComponent } from "./accounts/register.component";
@@ -60,6 +61,11 @@ const routes: Routes = [
canActivate: [HomeGuard], // Redirects either to vault, login or lock page.
},
{ path: "login", component: LoginComponent, canActivate: [UnauthGuard] },
+ {
+ path: "login-with-device",
+ component: LoginWithDeviceComponent,
+ data: { titleId: "loginWithDevice" },
+ },
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
{
path: "register",
diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts
index 0885d7d5d7..457200a0e9 100644
--- a/apps/web/src/app/oss.module.ts
+++ b/apps/web/src/app/oss.module.ts
@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
+import { LoginModule } from "./accounts/login/login.module";
import { TrialInitiationModule } from "./accounts/trial-initiation/trial-initiation.module";
import { OrganizationCreateModule } from "./organizations/create/organization-create.module";
import { OrganizationManageModule } from "./organizations/manage/organization-manage.module";
@@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
OrganizationManageModule,
OrganizationUserModule,
OrganizationCreateModule,
+ LoginModule,
],
exports: [
SharedModule,
@@ -25,6 +27,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
TrialInitiationModule,
VaultFilterModule,
OrganizationBadgeModule,
+ LoginModule,
],
bootstrap: [],
})
diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts
index 59315f9253..c68741b5a2 100644
--- a/apps/web/src/app/shared/loose-components.module.ts
+++ b/apps/web/src/app/shared/loose-components.module.ts
@@ -6,7 +6,6 @@ import { AcceptEmergencyComponent } from "../accounts/accept-emergency.component
import { AcceptOrganizationComponent } from "../accounts/accept-organization.component";
import { HintComponent } from "../accounts/hint.component";
import { LockComponent } from "../accounts/lock.component";
-import { LoginComponent } from "../accounts/login.component";
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
import { RegisterFormModule } from "../accounts/register-form/register-form.module";
@@ -210,7 +209,6 @@ import { SharedModule } from ".";
FrontendLayoutComponent,
HintComponent,
LockComponent,
- LoginComponent,
MasterPasswordPolicyComponent,
NavbarComponent,
NestedCheckboxComponent,
@@ -355,7 +353,6 @@ import { SharedModule } from ".";
FrontendLayoutComponent,
HintComponent,
LockComponent,
- LoginComponent,
MasterPasswordPolicyComponent,
NavbarComponent,
NestedCheckboxComponent,
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 80755183b9..3b8c2f772d 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -569,15 +569,27 @@
"loginOrCreateNewAccount": {
"message": "Log in or create a new account to access your secure vault."
},
+ "loginWithDevice" : {
+ "message": "Log in with device"
+ },
+ "loginWithDevciceEnabledInfo": {
+ "message": "Log in with device must be enabled in the settings of the Biwarden mobile app. Need another option?"
+ },
"createAccount": {
"message": "Create Account"
},
+ "newAroundHere": {
+ "message": "New around here?"
+ },
"startTrial": {
"message": "Start Trial"
},
"logIn": {
"message": "Log In"
},
+ "logInInitiated": {
+ "message": "Log in initiated"
+ },
"submit": {
"message": "Submit"
},
@@ -635,7 +647,7 @@
"confirmMasterPasswordRequired": {
"message": "Master password retype is required."
},
- "masterPasswordMinLength": {
+ "masterPasswordMinlength": {
"message": "Master password must be at least 8 characters long."
},
"masterPassDoesntMatch": {
@@ -705,6 +717,9 @@
"noOrganizationsList": {
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users."
},
+ "notificationSentDevice":{
+ "message": "A notification has been sent to your device."
+ },
"versionNumber": {
"message": "Version $VERSION_NUMBER$",
"placeholders": {
@@ -2532,6 +2547,9 @@
}
}
},
+ "viewAllLoginOptions": {
+ "message": "View all log in options"
+ },
"viewedItemId": {
"message": "Viewed item $ID$.",
"placeholders": {
@@ -3372,6 +3390,12 @@
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
+ "fingerprintMatchInfo": {
+ "message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device."
+ },
+ "fingerprintPhraseHeader": {
+ "message": "Fingerprint phrase"
+ },
"dontAskFingerprintAgain": {
"message": "Never prompt to verify fingerprint phrases for invited users (Not recommended)",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
@@ -4372,6 +4396,9 @@
"reinviteSelected": {
"message": "Resend Invitations"
},
+ "resendNotification": {
+ "message": "Resend notification"
+ },
"noSelectedUsersApplicable": {
"message": "This action is not applicable to any of the selected users."
},
diff --git a/apps/web/src/utils/flags.ts b/apps/web/src/utils/flags.ts
index 5cc3b930bb..195bc8e5f5 100644
--- a/apps/web/src/utils/flags.ts
+++ b/apps/web/src/utils/flags.ts
@@ -10,6 +10,7 @@ import {
/* eslint-disable-next-line @typescript-eslint/ban-types */
export type Flags = {
showTrial?: boolean;
+ showPasswordless?: boolean;
} & SharedFlags;
// required to avoid linting errors when there are no flags
diff --git a/libs/angular/src/components/login.component.ts b/libs/angular/src/components/login.component.ts
index 1c7a8c2332..1bc2e8ed87 100644
--- a/libs/angular/src/components/login.component.ts
+++ b/libs/angular/src/components/login.component.ts
@@ -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 { take } from "rxjs/operators";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
+import {
+ AllValidationErrors,
+ FormValidationErrorsService,
+} from "@bitwarden/common/abstractions/formValidationErrors.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";
@@ -18,16 +23,19 @@ import { CaptchaProtectedComponent } from "./captchaProtected.component";
@Directive()
export class LoginComponent extends CaptchaProtectedComponent implements OnInit {
- @Input() email = "";
- @Input() rememberEmail = true;
-
- masterPassword = "";
showPassword = false;
formPromise: Promise;
onSuccessfulLogin: () => Promise;
onSuccessfulLoginNavigate: () => Promise;
onSuccessfulLoginTwoFactorNavigate: () => Promise;
onSuccessfulLoginForceResetNavigate: () => Promise;
+ selfHosted = false;
+
+ formGroup = this.formBuilder.group({
+ email: ["", [Validators.required, Validators.email]],
+ masterPassword: ["", [Validators.required, Validators.minLength(8)]],
+ rememberEmail: [true],
+ });
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
@@ -44,9 +52,12 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
protected passwordGenerationService: PasswordGenerationService,
protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService,
- protected ngZone: NgZone
+ protected ngZone: NgZone,
+ protected formBuilder: FormBuilder,
+ protected formValidationErrorService: FormValidationErrorsService
) {
super(environmentService, i18nService, platformUtilsService);
+ this.selfHosted = platformUtilsService.isSelfHost();
}
get selfHostedDomain() {
@@ -54,59 +65,53 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
}
async ngOnInit() {
- if (this.email == null || this.email === "") {
- this.email = await this.stateService.getRememberedEmail();
- if (this.email == null) {
- this.email = "";
+ let email = this.formGroup.get("email")?.value;
+ if (email == null || email === "") {
+ email = await this.stateService.getRememberedEmail();
+ this.formGroup.get("email")?.setValue(email);
+
+ if (email == null) {
+ this.formGroup.get("email")?.setValue("");
}
}
if (!this.alwaysRememberEmail) {
- this.rememberEmail = (await this.stateService.getRememberedEmail()) != null;
- }
- if (Utils.isBrowser && !Utils.isNode) {
- this.focusInput();
+ const rememberEmail = (await this.stateService.getRememberedEmail()) != null;
+ this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
}
}
- 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();
- if (this.email == null || this.email === "") {
- this.platformUtilsService.showToast(
- "error",
- this.i18nService.t("errorOccurred"),
- this.i18nService.t("emailRequired")
- );
+ this.formGroup.markAllAsTouched();
+
+ //web
+ if (this.formGroup.invalid && !showToast) {
return;
}
- if (this.email.indexOf("@") === -1) {
- this.platformUtilsService.showToast(
- "error",
- this.i18nService.t("errorOccurred"),
- this.i18nService.t("invalidEmail")
- );
- return;
- }
- if (this.masterPassword == null || this.masterPassword === "") {
- this.platformUtilsService.showToast(
- "error",
- this.i18nService.t("errorOccurred"),
- this.i18nService.t("masterPasswordRequired")
- );
+
+ //desktop, browser; This should be removed once all clients use reactive forms
+ if (this.formGroup.invalid && showToast) {
+ const errorText = this.getErrorToastMessage();
+ this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
return;
}
try {
const credentials = new PasswordLogInCredentials(
- this.email,
- this.masterPassword,
+ email,
+ masterPassword,
this.captchaToken,
null
);
this.formPromise = this.authService.logIn(credentials);
const response = await this.formPromise;
- if (this.rememberEmail || this.alwaysRememberEmail) {
- await this.stateService.setRememberedEmail(this.email);
+ if (rememberEmail || this.alwaysRememberEmail) {
+ await this.stateService.setRememberedEmail(email);
} else {
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() {
- document
- .getElementById(this.email == null || this.email === "" ? "email" : "masterPassword")
- .focus();
+ const email = this.formGroup.get("email")?.value;
+ document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus();
}
}
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index b76ad9a590..b53aac0866 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -5,6 +5,7 @@ import { AbstractThemingService } from "@bitwarden/angular/services/theming/them
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
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 { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.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 { AccountApiService } from "@bitwarden/common/services/account/account-api.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 { AppIdService } from "@bitwarden/common/services/appId.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
@@ -544,6 +546,11 @@ import { ValidationService } from "./validation.service";
useClass: ConfigApiService,
deps: [ApiServiceAbstraction],
},
+ {
+ provide: AnonymousHubServiceAbstraction,
+ useClass: AnonymousHubService,
+ deps: [EnvironmentServiceAbstraction, AuthServiceAbstraction, LogService],
+ },
],
})
export class JslibServicesModule {}
diff --git a/libs/common/src/abstractions/anonymousHub.service.ts b/libs/common/src/abstractions/anonymousHub.service.ts
new file mode 100644
index 0000000000..43bdabd512
--- /dev/null
+++ b/libs/common/src/abstractions/anonymousHub.service.ts
@@ -0,0 +1,4 @@
+export abstract class AnonymousHubService {
+ createHubConnection: (token: string) => void;
+ stopHubConnection: () => void;
+}
diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts
index f08e5c34af..06d3f5b4eb 100644
--- a/libs/common/src/abstractions/api.service.ts
+++ b/libs/common/src/abstractions/api.service.ts
@@ -46,6 +46,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
import { PasswordRequest } from "../models/request/passwordRequest";
+import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest";
import { PaymentRequest } from "../models/request/paymentRequest";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
@@ -84,6 +85,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
import { AttachmentResponse } from "../models/response/attachmentResponse";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
+import { AuthRequestResponse } from "../models/response/authRequestResponse";
import { RegisterResponse } from "../models/response/authentication/registerResponse";
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
@@ -210,6 +212,9 @@ export abstract class ApiService {
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise;
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise;
postConvertToKeyConnector: () => Promise;
+ //passwordless
+ postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise;
+ getAuthResponse: (id: string, accessCode: string) => Promise;
getUserBillingHistory: () => Promise;
getUserBillingPayment: () => Promise;
diff --git a/libs/common/src/abstractions/auth.service.ts b/libs/common/src/abstractions/auth.service.ts
index 4947f21708..bbe1c01bf2 100644
--- a/libs/common/src/abstractions/auth.service.ts
+++ b/libs/common/src/abstractions/auth.service.ts
@@ -1,18 +1,26 @@
+import { Observable } from "rxjs";
+
import { AuthenticationStatus } from "../enums/authenticationStatus";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
+ PasswordlessLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
+import { AuthRequestPushNotification } from "../models/response/notificationResponse";
export abstract class AuthService {
masterPasswordHash: string;
email: string;
logIn: (
- credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
+ credentials:
+ | ApiLogInCredentials
+ | PasswordLogInCredentials
+ | SsoLogInCredentials
+ | PasswordlessLogInCredentials
) => Promise;
logInTwoFactor: (
twoFactor: TokenRequestTwoFactor,
@@ -24,4 +32,7 @@ export abstract class AuthService {
authingWithSso: () => boolean;
authingWithPassword: () => boolean;
getAuthStatus: (userId?: string) => Promise;
+ authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise;
+
+ getPushNotifcationObs$: () => Observable;
}
diff --git a/libs/common/src/enums/authRequestType.ts b/libs/common/src/enums/authRequestType.ts
new file mode 100644
index 0000000000..4edfa5b888
--- /dev/null
+++ b/libs/common/src/enums/authRequestType.ts
@@ -0,0 +1,4 @@
+export enum AuthRequestType {
+ AuthenticateAndUnlock = 0,
+ Unlock = 1,
+}
diff --git a/libs/common/src/enums/authenticationType.ts b/libs/common/src/enums/authenticationType.ts
index ed7375c808..5133c4f648 100644
--- a/libs/common/src/enums/authenticationType.ts
+++ b/libs/common/src/enums/authenticationType.ts
@@ -2,4 +2,5 @@ export enum AuthenticationType {
Password = 0,
Sso = 1,
Api = 2,
+ Passwordless = 3,
}
diff --git a/libs/common/src/enums/notificationType.ts b/libs/common/src/enums/notificationType.ts
index 77ebde01fc..457ad174ca 100644
--- a/libs/common/src/enums/notificationType.ts
+++ b/libs/common/src/enums/notificationType.ts
@@ -17,4 +17,7 @@ export enum NotificationType {
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
+
+ AuthRequest = 15,
+ AuthRequestResponse = 16,
}
diff --git a/libs/common/src/misc/logInStrategies/logIn.strategy.ts b/libs/common/src/misc/logInStrategies/logIn.strategy.ts
index 8615700681..577130156f 100644
--- a/libs/common/src/misc/logInStrategies/logIn.strategy.ts
+++ b/libs/common/src/misc/logInStrategies/logIn.strategy.ts
@@ -14,6 +14,7 @@ import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
+ PasswordlessLogInCredentials,
} from "../../models/domain/logInCredentials";
import { DeviceRequest } from "../../models/request/deviceRequest";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
@@ -42,7 +43,11 @@ export abstract class LogInStrategy {
) {}
abstract logIn(
- credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
+ credentials:
+ | ApiLogInCredentials
+ | PasswordLogInCredentials
+ | SsoLogInCredentials
+ | PasswordlessLogInCredentials
): Promise;
async logInTwoFactor(
diff --git a/libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts b/libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts
new file mode 100644
index 0000000000..0acc4a49f0
--- /dev/null
+++ b/libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts
@@ -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 {
+ 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();
+ }
+}
diff --git a/libs/common/src/models/domain/logInCredentials.ts b/libs/common/src/models/domain/logInCredentials.ts
index c1e23610e4..5f2035fd15 100644
--- a/libs/common/src/models/domain/logInCredentials.ts
+++ b/libs/common/src/models/domain/logInCredentials.ts
@@ -1,3 +1,5 @@
+import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
+
import { AuthenticationType } from "../../enums/authenticationType";
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
@@ -29,3 +31,16 @@ export class ApiLogInCredentials {
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
+ ) {}
+}
diff --git a/libs/common/src/models/request/identityToken/tokenRequest.ts b/libs/common/src/models/request/identityToken/tokenRequest.ts
index 82a4a394c5..5e38d2069b 100644
--- a/libs/common/src/models/request/identityToken/tokenRequest.ts
+++ b/libs/common/src/models/request/identityToken/tokenRequest.ts
@@ -4,6 +4,7 @@ import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
export abstract class TokenRequest {
protected device?: DeviceRequest;
+ protected passwordlessAuthRequest: string;
constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) {
this.device = device != null ? device : null;
@@ -18,6 +19,10 @@ export abstract class TokenRequest {
this.twoFactor = twoFactor;
}
+ setPasswordlessAccessCode(accessCode: string) {
+ this.passwordlessAuthRequest = accessCode;
+ }
+
protected toIdentityToken(clientId: string) {
const obj: any = {
scope: "api offline_access",
@@ -32,6 +37,11 @@ export abstract class TokenRequest {
// obj.devicePushToken = this.device.pushToken;
}
+ //passswordless login
+ if (this.passwordlessAuthRequest) {
+ obj.authRequest = this.passwordlessAuthRequest;
+ }
+
if (this.twoFactor.token && this.twoFactor.provider != null) {
obj.twoFactorToken = this.twoFactor.token;
obj.twoFactorProvider = this.twoFactor.provider;
diff --git a/libs/common/src/models/request/passwordlessCreateAuthRequest.ts b/libs/common/src/models/request/passwordlessCreateAuthRequest.ts
new file mode 100644
index 0000000000..df83c54777
--- /dev/null
+++ b/libs/common/src/models/request/passwordlessCreateAuthRequest.ts
@@ -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
+ ) {}
+}
diff --git a/libs/common/src/models/response/authRequestResponse.ts b/libs/common/src/models/response/authRequestResponse.ts
new file mode 100644
index 0000000000..1a29a3da85
--- /dev/null
+++ b/libs/common/src/models/response/authRequestResponse.ts
@@ -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");
+ }
+}
diff --git a/libs/common/src/models/response/notificationResponse.ts b/libs/common/src/models/response/notificationResponse.ts
index f23de8fe8b..1e2a504506 100644
--- a/libs/common/src/models/response/notificationResponse.ts
+++ b/libs/common/src/models/response/notificationResponse.ts
@@ -37,6 +37,10 @@ export class NotificationResponse extends BaseResponse {
case NotificationType.SyncSendDelete:
this.payload = new SyncSendNotification(payload);
break;
+ case NotificationType.AuthRequest:
+ case NotificationType.AuthRequestResponse:
+ this.payload = new AuthRequestPushNotification(payload);
+ break;
default:
break;
}
@@ -96,3 +100,14 @@ export class SyncSendNotification extends BaseResponse {
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");
+ }
+}
diff --git a/libs/common/src/services/anonymousHub.service.ts b/libs/common/src/services/anonymousHub.service.ts
new file mode 100644
index 0000000000..13b5898b18
--- /dev/null
+++ b/libs/common/src/services/anonymousHub.service.ts
@@ -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
+ );
+ }
+}
diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts
index 0b96a11831..a4648e2504 100644
--- a/libs/common/src/services/api.service.ts
+++ b/libs/common/src/services/api.service.ts
@@ -54,6 +54,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat
import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest";
import { PasswordHintRequest } from "../models/request/passwordHintRequest";
import { PasswordRequest } from "../models/request/passwordRequest";
+import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest";
import { PaymentRequest } from "../models/request/paymentRequest";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest";
@@ -92,6 +93,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
import { AttachmentResponse } from "../models/response/attachmentResponse";
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
+import { AuthRequestResponse } from "../models/response/authRequestResponse";
import { RegisterResponse } from "../models/response/authentication/registerResponse";
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
@@ -265,6 +267,17 @@ export class ApiService implements ApiServiceAbstraction {
}
}
+ async postAuthRequest(request: PasswordlessCreateAuthRequest): Promise {
+ const r = await this.send("POST", "/auth-requests/", request, false, true);
+ return new AuthRequestResponse(r);
+ }
+
+ async getAuthResponse(id: string, accessCode: string): Promise {
+ const path = `/auth-requests/${id}/response?code=${accessCode}`;
+ const r = await this.send("GET", path, null, false, true);
+ return new AuthRequestResponse(r);
+ }
+
// Account APIs
async getProfile(): Promise {
diff --git a/libs/common/src/services/auth.service.ts b/libs/common/src/services/auth.service.ts
index 6f77bca20b..3807eee3d6 100644
--- a/libs/common/src/services/auth.service.ts
+++ b/libs/common/src/services/auth.service.ts
@@ -1,3 +1,5 @@
+import { Observable, Subject } from "rxjs";
+
import { ApiService } from "../abstractions/api.service";
import { AppIdService } from "../abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
@@ -17,17 +19,20 @@ import { KdfType } from "../enums/kdfType";
import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
+import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy";
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
+ PasswordlessLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ErrorResponse } from "../models/response/errorResponse";
+import { AuthRequestPushNotification } from "../models/response/notificationResponse";
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
@@ -42,9 +47,15 @@ export class AuthService implements AuthServiceAbstraction {
: null;
}
- private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
+ private logInStrategy:
+ | ApiLogInStrategy
+ | PasswordLogInStrategy
+ | SsoLogInStrategy
+ | PasswordlessLogInStrategy;
private sessionTimeout: any;
+ private pushNotificationSubject = new Subject();
+
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
@@ -61,52 +72,78 @@ export class AuthService implements AuthServiceAbstraction {
) {}
async logIn(
- credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
+ credentials:
+ | ApiLogInCredentials
+ | PasswordLogInCredentials
+ | SsoLogInCredentials
+ | PasswordlessLogInCredentials
): Promise {
this.clearState();
- let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
+ let strategy:
+ | ApiLogInStrategy
+ | PasswordLogInStrategy
+ | SsoLogInStrategy
+ | PasswordlessLogInStrategy;
- if (credentials.type === AuthenticationType.Password) {
- strategy = new PasswordLogInStrategy(
- this.cryptoService,
- this.apiService,
- this.tokenService,
- this.appIdService,
- this.platformUtilsService,
- this.messagingService,
- this.logService,
- this.stateService,
- this.twoFactorService,
- this
- );
- } else if (credentials.type === AuthenticationType.Sso) {
- strategy = new SsoLogInStrategy(
- this.cryptoService,
- this.apiService,
- this.tokenService,
- this.appIdService,
- this.platformUtilsService,
- this.messagingService,
- this.logService,
- this.stateService,
- this.twoFactorService,
- this.keyConnectorService
- );
- } else if (credentials.type === AuthenticationType.Api) {
- strategy = new ApiLogInStrategy(
- this.cryptoService,
- this.apiService,
- this.tokenService,
- this.appIdService,
- this.platformUtilsService,
- this.messagingService,
- this.logService,
- this.stateService,
- this.twoFactorService,
- this.environmentService,
- this.keyConnectorService
- );
+ switch (credentials.type) {
+ case AuthenticationType.Password:
+ strategy = new PasswordLogInStrategy(
+ this.cryptoService,
+ this.apiService,
+ this.tokenService,
+ this.appIdService,
+ this.platformUtilsService,
+ this.messagingService,
+ this.logService,
+ this.stateService,
+ this.twoFactorService,
+ this
+ );
+ break;
+ case AuthenticationType.Sso:
+ strategy = new SsoLogInStrategy(
+ this.cryptoService,
+ this.apiService,
+ this.tokenService,
+ this.appIdService,
+ this.platformUtilsService,
+ this.messagingService,
+ this.logService,
+ this.stateService,
+ this.twoFactorService,
+ this.keyConnectorService
+ );
+ break;
+ case AuthenticationType.Api:
+ strategy = new ApiLogInStrategy(
+ this.cryptoService,
+ this.apiService,
+ this.tokenService,
+ this.appIdService,
+ this.platformUtilsService,
+ this.messagingService,
+ this.logService,
+ 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);
@@ -202,7 +239,21 @@ export class AuthService implements AuthServiceAbstraction {
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
}
- private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
+ async authResponsePushNotifiction(notification: AuthRequestPushNotification): Promise {
+ this.pushNotificationSubject.next(notification.id);
+ }
+
+ getPushNotifcationObs$(): Observable {
+ return this.pushNotificationSubject.asObservable();
+ }
+
+ private saveState(
+ strategy:
+ | ApiLogInStrategy
+ | PasswordLogInStrategy
+ | SsoLogInStrategy
+ | PasswordlessLogInStrategy
+ ) {
this.logInStrategy = strategy;
this.startSessionTimeout();
}