From 9429ae1d068fbe9dec7a38434fc69f06df63491e Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:53:01 -0800 Subject: [PATCH] feat(auth): [PM-9723] Refresh LoginViaAuthRequestComponent (#11545) Creates a refreshed and consolidated LoginViaAuthRequestComponent for use on all visual clients, which will be used when the UnauthenticatedExtensionUIRefresh feature flag is on. --- apps/browser/src/_locales/en/messages.json | 15 + ... login-via-auth-request-v1.component.html} | 0 ...=> login-via-auth-request-v1.component.ts} | 6 +- .../src/popup/app-routing.animations.ts | 3 + apps/browser/src/popup/app-routing.module.ts | 71 ++- apps/browser/src/popup/app.module.ts | 4 +- apps/desktop/src/app/app-routing.module.ts | 59 +- ... login-via-auth-request-v1.component.html} | 0 ...=> login-via-auth-request-v1.component.ts} | 6 +- apps/desktop/src/auth/login/login.module.ts | 6 +- apps/desktop/src/locales/en/messages.json | 15 + ... login-via-auth-request-v1.component.html} | 1 + .../login-via-auth-request-v1.component.ts | 9 + .../login/login-via-auth-request.component.ts | 9 - apps/web/src/app/auth/login/login.module.ts | 6 +- apps/web/src/app/oss-routing.module.ts | 65 +- apps/web/src/locales/en/messages.json | 15 + ...=> login-via-auth-request-v1.component.ts} | 8 +- .../src/services/jslib-services.module.ts | 7 + libs/auth/src/angular/icons/devices.icon.ts | 52 ++ libs/auth/src/angular/icons/index.ts | 1 + libs/auth/src/angular/index.ts | 3 + .../login-via-auth-request.component.html | 41 ++ .../login-via-auth-request.component.ts | 569 ++++++++++++++++++ .../abstractions/auth-request-api.service.ts | 37 ++ libs/auth/src/common/abstractions/index.ts | 1 + .../auth-request/auth-request-api.service.ts | 65 ++ libs/auth/src/common/services/index.ts | 1 + libs/common/src/abstractions/api.service.ts | 6 +- ...create-auth.request.ts => auth.request.ts} | 2 +- .../models/response/auth-request.response.ts | 4 +- libs/common/src/services/api.service.ts | 7 +- 32 files changed, 1025 insertions(+), 69 deletions(-) rename apps/browser/src/auth/popup/{login-via-auth-request.component.html => login-via-auth-request-v1.component.html} (100%) rename apps/browser/src/auth/popup/{login-via-auth-request.component.ts => login-via-auth-request-v1.component.ts} (91%) rename apps/desktop/src/auth/login/{login-via-auth-request.component.html => login-via-auth-request-v1.component.html} (100%) rename apps/desktop/src/auth/login/{login-via-auth-request.component.ts => login-via-auth-request-v1.component.ts} (93%) rename apps/web/src/app/auth/login/{login-via-auth-request.component.html => login-via-auth-request-v1.component.html} (99%) create mode 100644 apps/web/src/app/auth/login/login-via-auth-request-v1.component.ts delete mode 100644 apps/web/src/app/auth/login/login-via-auth-request.component.ts rename libs/angular/src/auth/components/{login-via-auth-request.component.ts => login-via-auth-request-v1.component.ts} (99%) create mode 100644 libs/auth/src/angular/icons/devices.icon.ts create mode 100644 libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html create mode 100644 libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts create mode 100644 libs/auth/src/common/abstractions/auth-request-api.service.ts create mode 100644 libs/auth/src/common/services/auth-request/auth-request-api.service.ts rename libs/common/src/auth/models/request/{create-auth.request.ts => auth.request.ts} (88%) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index df4b9a9001..26fe9186cf 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3170,12 +3170,27 @@ "resendNotification": { "message": "Resend notification" }, + "viewAllLogInOptions": { + "message": "View all log in options" + }, "viewAllLoginOptions": { "message": "View all log in options" }, "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "aNotificationWasSentToYourDevice": { + "message": "A notification was sent to your device" + }, + "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { + "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + }, + "youWillBeNotifiedOnceTheRequestIsApproved": { + "message": "You will be notified once the request is approved" + }, + "needAnotherOptionV1": { + "message": "Need another option?" + }, "loginInitiated": { "message": "Login initiated" }, diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.html b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html similarity index 100% rename from apps/browser/src/auth/popup/login-via-auth-request.component.html rename to apps/browser/src/auth/popup/login-via-auth-request-v1.component.html diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts similarity index 91% rename from apps/browser/src/auth/popup/login-via-auth-request.component.ts rename to apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts index 9dc0d7d545..66c69d0a41 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.ts @@ -2,7 +2,7 @@ import { Location } from "@angular/common"; import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; +import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, @@ -27,9 +27,9 @@ import { KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-login-via-auth-request", - templateUrl: "login-via-auth-request.component.html", + templateUrl: "login-via-auth-request-v1.component.html", }) -export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { +export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 { constructor( router: Router, keyService: KeyService, diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 90990ea832..061067c717 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -132,6 +132,9 @@ export const routerTransition = trigger("routerTransition", [ transition("login-with-device => tabs, login-with-device => 2fa", inSlideLeft), transition("login-with-device => login", outSlideRight), + transition("admin-approval-requested => tabs, admin-approval-requested => 2fa", inSlideLeft), + transition("admin-approval-requested => login", outSlideRight), + transition(tabsToCiphers, inSlideLeft), transition(ciphersToTabs, outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index d53e51e9df..ba8ab1e7aa 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -21,10 +21,12 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + DevicesIcon, LoginComponent, LoginSecondaryContentComponent, LockIcon, LockV2Component, + LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, RegistrationLockAltIcon, @@ -51,7 +53,7 @@ import { HomeComponent } from "../auth/popup/home.component"; import { LockComponent } from "../auth/popup/lock.component"; import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component"; import { LoginComponentV1 } from "../auth/popup/login-v1.component"; -import { LoginViaAuthRequestComponent } from "../auth/popup/login-via-auth-request.component"; +import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; @@ -171,18 +173,6 @@ const routes: Routes = [ canActivate: [fido2AuthGuard], data: { state: "fido2" } satisfies RouteDataProperties, }), - { - path: "login-with-device", - component: LoginViaAuthRequestComponent, - canActivate: [], - data: { state: "login-with-device" } satisfies RouteDataProperties, - }, - { - path: "admin-approval-requested", - component: LoginViaAuthRequestComponent, - canActivate: [], - data: { state: "login-with-device" } satisfies RouteDataProperties, - }, { path: "lock", component: LockComponent, @@ -409,6 +399,61 @@ const routes: Routes = [ canActivate: [authGuard], data: { state: "update-temp-password" } satisfies RouteDataProperties, }, + ...unauthUiRefreshSwap( + LoginViaAuthRequestComponentV1, + ExtensionAnonLayoutWrapperComponent, + { + path: "login-with-device", + data: { state: "login-with-device" } satisfies RouteDataProperties, + }, + { + path: "login-with-device", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "loginInitiated", + }, + pageSubtitle: { + key: "aNotificationWasSentToYourDevice", + }, + showLogo: false, + showBackButton: true, + state: "login-with-device", + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaAuthRequestComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ), + ...unauthUiRefreshSwap( + LoginViaAuthRequestComponentV1, + ExtensionAnonLayoutWrapperComponent, + { + path: "admin-approval-requested", + data: { state: "admin-approval-requested" } satisfies RouteDataProperties, + }, + { + path: "admin-approval-requested", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "adminApprovalRequested", + }, + pageSubtitle: { + key: "adminApprovalRequestSentToAdmins", + }, + showLogo: false, + showBackButton: true, + state: "admin-approval-requested", + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [{ path: "", component: LoginViaAuthRequestComponent }], + }, + ), ...unauthUiRefreshSwap( HintComponent, ExtensionAnonLayoutWrapperComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 2d0ccd1d1c..d6e46de6ba 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -26,7 +26,7 @@ import { HomeComponent } from "../auth/popup/home.component"; import { LockComponent } from "../auth/popup/lock.component"; import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component"; import { LoginComponentV1 } from "../auth/popup/login-v1.component"; -import { LoginViaAuthRequestComponent } from "../auth/popup/login-via-auth-request.component"; +import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; @@ -159,8 +159,8 @@ import "../platform/popup/locales"; HintComponent, HomeComponent, LockComponent, + LoginViaAuthRequestComponentV1, LoginComponentV1, - LoginViaAuthRequestComponent, LoginDecryptionOptionsComponent, NotificationsSettingsV1Component, AppearanceComponent, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index f5023cb424..c1e4fd1869 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -18,10 +18,12 @@ import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-ref import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + DevicesIcon, LoginComponent, LoginSecondaryContentComponent, LockIcon, LockV2Component, + LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, RegistrationLockAltIcon, @@ -42,7 +44,7 @@ import { HintComponent } from "../auth/hint.component"; import { LockComponent } from "../auth/lock.component"; import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component"; import { LoginComponentV1 } from "../auth/login/login-v1.component"; -import { LoginViaAuthRequestComponent } from "../auth/login/login-via-auth-request.component"; +import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component"; import { RegisterComponent } from "../auth/register.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; @@ -75,14 +77,6 @@ const routes: Routes = [ canActivate: [lockGuard()], canMatch: [extensionRefreshRedirect("/lockV2")], }, - { - path: "login-with-device", - component: LoginViaAuthRequestComponent, - }, - { - path: "admin-approval-requested", - component: LoginViaAuthRequestComponent, - }, ...twofactorRefactorSwap( TwoFactorComponent, AnonLayoutWrapperComponent, @@ -130,6 +124,53 @@ const routes: Routes = [ component: RemovePasswordComponent, canActivate: [authGuard], }, + ...unauthUiRefreshSwap( + LoginViaAuthRequestComponentV1, + AnonLayoutWrapperComponent, + { + path: "login-with-device", + }, + { + path: "login-with-device", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "loginInitiated", + }, + pageSubtitle: { + key: "aNotificationWasSentToYourDevice", + }, + } satisfies AnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaAuthRequestComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ), + ...unauthUiRefreshSwap( + LoginViaAuthRequestComponentV1, + AnonLayoutWrapperComponent, + { + path: "admin-approval-requested", + }, + { + path: "admin-approval-requested", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "adminApprovalRequested", + }, + pageSubtitle: { + key: "adminApprovalRequestSentToAdmins", + }, + } satisfies AnonLayoutWrapperData, + children: [{ path: "", component: LoginViaAuthRequestComponent }], + }, + ), ...unauthUiRefreshSwap( HintComponent, AnonLayoutWrapperComponent, diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.html b/apps/desktop/src/auth/login/login-via-auth-request-v1.component.html similarity index 100% rename from apps/desktop/src/auth/login/login-via-auth-request.component.html rename to apps/desktop/src/auth/login/login-via-auth-request-v1.component.html diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts similarity index 93% rename from apps/desktop/src/auth/login/login-via-auth-request.component.ts rename to apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts index 8459dc7441..b59e077837 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request-v1.component.ts @@ -2,7 +2,7 @@ import { Location } from "@angular/common"; import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { Router } from "@angular/router"; -import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; +import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuthRequestServiceAbstraction, @@ -30,9 +30,9 @@ import { EnvironmentComponent } from "../environment.component"; @Component({ selector: "app-login-via-auth-request", - templateUrl: "login-via-auth-request.component.html", + templateUrl: "login-via-auth-request-v1.component.html", }) -export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { +export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 { @ViewChild("environment", { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; showingModal = false; diff --git a/apps/desktop/src/auth/login/login.module.ts b/apps/desktop/src/auth/login/login.module.ts index c0b330bf2d..20c0bc97c6 100644 --- a/apps/desktop/src/auth/login/login.module.ts +++ b/apps/desktop/src/auth/login/login.module.ts @@ -7,16 +7,16 @@ import { SharedModule } from "../../app/shared/shared.module"; import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component"; import { LoginComponentV1 } from "./login-v1.component"; -import { LoginViaAuthRequestComponent } from "./login-via-auth-request.component"; +import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component"; @NgModule({ imports: [SharedModule, RouterModule], declarations: [ LoginComponentV1, - LoginViaAuthRequestComponent, + LoginViaAuthRequestComponentV1, EnvironmentSelectorComponent, LoginDecryptionOptionsComponent, ], - exports: [LoginComponentV1, LoginViaAuthRequestComponent], + exports: [LoginComponentV1, LoginViaAuthRequestComponentV1], }) export class LoginModule {} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 78358bc009..e9f26d23e9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2689,15 +2689,30 @@ "notificationSentDevice": { "message": "A notification has been sent to your device." }, + "aNotificationWasSentToYourDevice": { + "message": "A notification was sent to your device" + }, + "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { + "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + }, + "needAnotherOptionV1": { + "message": "Need another option?" + }, "fingerprintMatchInfo": { "message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device." }, "fingerprintPhraseHeader": { "message": "Fingerprint phrase" }, + "youWillBeNotifiedOnceTheRequestIsApproved": { + "message": "You will be notified once the request is approved" + }, "needAnotherOption": { "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" }, + "viewAllLogInOptions": { + "message": "View all log in options" + }, "viewAllLoginOptions": { "message": "View all login options" }, diff --git a/apps/web/src/app/auth/login/login-via-auth-request.component.html b/apps/web/src/app/auth/login/login-via-auth-request-v1.component.html similarity index 99% rename from apps/web/src/app/auth/login/login-via-auth-request.component.html rename to apps/web/src/app/auth/login/login-via-auth-request-v1.component.html index 0b3f09afc4..69777950a7 100644 --- a/apps/web/src/app/auth/login/login-via-auth-request.component.html +++ b/apps/web/src/app/auth/login/login-via-auth-request-v1.component.html @@ -45,6 +45,7 @@ +
Promise; onSuccessfulLogin: () => Promise; @@ -265,7 +265,7 @@ export class LoginViaAuthRequestComponent this.authRequestKeyPair.publicKey, ); - this.authRequest = new CreateAuthRequest( + this.authRequest = new AuthRequest( this.email, deviceIdentifier, publicKey, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 340e8f567c..251223d1be 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -31,6 +31,8 @@ import { UserDecryptionOptionsServiceAbstraction, LogoutReason, RegisterRouteService, + AuthRequestApiService, + DefaultAuthRequestApiService, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -1377,6 +1379,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCipherAuthorizationService, deps: [CollectionService, OrganizationServiceAbstraction], }), + safeProvider({ + provide: AuthRequestApiService, + useClass: DefaultAuthRequestApiService, + deps: [ApiServiceAbstraction, LogService], + }), ]; @NgModule({ diff --git a/libs/auth/src/angular/icons/devices.icon.ts b/libs/auth/src/angular/icons/devices.icon.ts new file mode 100644 index 0000000000..54acea5b08 --- /dev/null +++ b/libs/auth/src/angular/icons/devices.icon.ts @@ -0,0 +1,52 @@ +import { svgIcon } from "@bitwarden/components"; + +export const DevicesIcon = svgIcon` + + + + + + + + + + +`; diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 70460a7aea..9c444df570 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1,5 +1,6 @@ export * from "./bitwarden-logo.icon"; export * from "./bitwarden-shield.icon"; +export * from "./devices.icon"; export * from "./lock.icon"; export * from "./registration-check-email.icon"; export * from "./user-lock.icon"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index d3d9e60091..5c028065c6 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -24,6 +24,9 @@ export * from "./login/login-secondary-content.component"; export * from "./login/login-component.service"; export * from "./login/default-login-component.service"; +// login via auth request +export * from "./login-via-auth-request/login-via-auth-request.component"; + // password callout export * from "./password-callout/password-callout.component"; diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html new file mode 100644 index 0000000000..a1d0f200c1 --- /dev/null +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.html @@ -0,0 +1,41 @@ +
+ +

{{ "makeSureYourAccountIsUnlockedAndTheFingerprintEtc" | i18n }}

+ +
{{ "fingerprintPhraseHeader" | i18n }}
+ {{ fingerprintPhrase }} + + + +
+ {{ "needAnotherOptionV1" | i18n }} + {{ + "viewAllLogInOptions" | i18n + }} +
+
+ + +

{{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}

+ +
{{ "fingerprintPhraseHeader" | i18n }}
+ {{ fingerprintPhrase }} + +
+ {{ "troubleLoggingIn" | i18n }} + {{ + "viewAllLogInOptions" | i18n + }} +
+
+
diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts new file mode 100644 index 0000000000..38614a9046 --- /dev/null +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -0,0 +1,569 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { IsActiveMatchOptions, Router, RouterModule } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + AuthRequestLoginCredentials, + AuthRequestServiceAbstraction, + LoginEmailServiceAbstraction, + LoginStrategyServiceAbstraction, +} from "@bitwarden/auth/common"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; +import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; + +import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service"; + +enum Flow { + StandardAuthRequest, // when user clicks "Login with device" from /login or "Approve from your other device" from /login-initiated + AdminAuthRequest, // when user clicks "Request admin approval" from /login-initiated +} + +const matchOptions: IsActiveMatchOptions = { + paths: "exact", + queryParams: "ignored", + fragment: "ignored", + matrixParams: "ignored", +}; + +@Component({ + standalone: true, + templateUrl: "./login-via-auth-request.component.html", + imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule], +}) +export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { + private authRequest: AuthRequest; + private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; + private authStatus: AuthenticationStatus; + private showResendNotificationTimeoutSeconds = 12; + + protected backToRoute = "/login"; + protected clientType: ClientType; + protected ClientType = ClientType; + protected email: string; + protected fingerprintPhrase: string; + protected showResendNotification = false; + protected Flow = Flow; + protected flow = Flow.StandardAuthRequest; + + constructor( + private accountService: AccountService, + private anonymousHubService: AnonymousHubService, + private appIdService: AppIdService, + private authRequestApiService: AuthRequestApiService, + private authRequestService: AuthRequestServiceAbstraction, + private authService: AuthService, + private cryptoFunctionService: CryptoFunctionService, + private deviceTrustService: DeviceTrustServiceAbstraction, + private i18nService: I18nService, + private logService: LogService, + private loginEmailService: LoginEmailServiceAbstraction, + private loginStrategyService: LoginStrategyServiceAbstraction, + private passwordGenerationService: PasswordGenerationServiceAbstraction, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private syncService: SyncService, + private toastService: ToastService, + private validationService: ValidationService, + ) { + this.clientType = this.platformUtilsService.getClientType(); + + // Gets SignalR push notification + // Only fires on approval to prevent enumeration + this.authRequestService.authRequestPushNotification$ + .pipe(takeUntilDestroyed()) + .subscribe((requestId) => { + this.verifyAndHandleApprovedAuthReq(requestId).catch((e: Error) => { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("error"), + message: e.message, + }); + + this.logService.error("Failed to use approved auth request: " + e.message); + }); + }); + } + + async ngOnInit(): Promise { + // Get the authStatus early because we use it in both flows + this.authStatus = await firstValueFrom(this.authService.activeAccountStatus$); + + const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked; + + if (userHasAuthenticatedViaSSO) { + this.backToRoute = "/login-initiated"; + } + + /** + * The LoginViaAuthRequestComponent handles both the `login-with-device` and + * the `admin-approval-requested` routes. Therefore we check the route to determine + * which flow to initialize. + */ + if (this.router.isActive("admin-approval-requested", matchOptions)) { + await this.initAdminAuthRequestFlow(); + } else { + await this.initStandardAuthRequestFlow(); + } + } + + private async initAdminAuthRequestFlow(): Promise { + this.flow = Flow.AdminAuthRequest; + + // Get email from state for admin auth requests because it is available and also + // prevents it from being lost on refresh as the loginEmailService email does not persist. + this.email = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.email)), + ); + + if (!this.email) { + await this.handleMissingEmail(); + return; + } + + // We only allow a single admin approval request to be active at a time + // so we must check state to see if we have an existing one or not + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const existingAdminAuthRequest = await this.authRequestService.getAdminAuthRequest(userId); + + if (existingAdminAuthRequest) { + await this.handleExistingAdminAuthRequest(existingAdminAuthRequest, userId); + } else { + await this.startAdminAuthRequestLogin(); + } + } + + private async initStandardAuthRequestFlow(): Promise { + this.flow = Flow.StandardAuthRequest; + + this.email = await firstValueFrom(this.loginEmailService.loginEmail$); + + if (!this.email) { + await this.handleMissingEmail(); + return; + } + + await this.startStandardAuthRequestLogin(); + } + + private async handleMissingEmail(): Promise { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("userEmailMissing"), + }); + + await this.router.navigate([this.backToRoute]); + } + + async ngOnDestroy(): Promise { + await this.anonymousHubService.stopHubConnection(); + } + + private async startAdminAuthRequestLogin(): Promise { + try { + await this.buildAuthRequest(AuthRequestType.AdminApproval); + + const authRequestResponse = await this.authRequestApiService.postAdminAuthRequest( + this.authRequest, + ); + const adminAuthReqStorable = new AdminAuthRequestStorable({ + id: authRequestResponse.id, + privateKey: this.authRequestKeyPair.privateKey, + }); + + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId); + + if (authRequestResponse.id) { + await this.anonymousHubService.createHubConnection(authRequestResponse.id); + } + } catch (e) { + this.logService.error(e); + } + } + + protected async startStandardAuthRequestLogin(): Promise { + this.showResendNotification = false; + + try { + await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); + + const authRequestResponse = await this.authRequestApiService.postAuthRequest( + this.authRequest, + ); + + if (authRequestResponse.id) { + await this.anonymousHubService.createHubConnection(authRequestResponse.id); + } + } catch (e) { + this.logService.error(e); + } + + setTimeout(() => { + this.showResendNotification = true; + }, this.showResendNotificationTimeoutSeconds * 1000); + } + + private async buildAuthRequest(authRequestType: AuthRequestType): Promise { + const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); + + this.authRequestKeyPair = { + publicKey: authRequestKeyPairArray[0], + privateKey: authRequestKeyPairArray[1], + }; + + const deviceIdentifier = await this.appIdService.getAppId(); + const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey); + const accessCode = await this.passwordGenerationService.generatePassword({ + type: "password", + length: 25, + }); + + this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( + this.email, + this.authRequestKeyPair.publicKey, + ); + + this.authRequest = new AuthRequest( + this.email, + deviceIdentifier, + publicKey, + authRequestType, + accessCode, + ); + } + + private async handleExistingAdminAuthRequest( + adminAuthRequestStorable: AdminAuthRequestStorable, + userId: UserId, + ): Promise { + // Note: on login, the SSOLoginStrategy will also call to see if an existing admin auth req + // has been approved and handle it if so. + + // Regardless, we always retrieve the auth request from the server and verify and handle status changes here as well + let adminAuthRequestResponse: AuthRequestResponse; + + try { + adminAuthRequestResponse = await this.authRequestApiService.getAuthRequest( + adminAuthRequestStorable.id, + ); + } catch (error) { + if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); + } + } + + // Request doesn't exist anymore + if (!adminAuthRequestResponse) { + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); + } + + // Re-derive the user's fingerprint phrase + // It is important to not use the server's public key here as it could have been compromised via MITM + const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey( + adminAuthRequestStorable.privateKey, + ); + this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase( + this.email, + derivedPublicKeyArrayBuffer, + ); + + // Request denied + if (adminAuthRequestResponse.isAnswered && !adminAuthRequestResponse.requestApproved) { + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); + } + + // Request approved + if (adminAuthRequestResponse.requestApproved) { + return await this.decryptViaApprovedAuthRequest( + adminAuthRequestResponse, + adminAuthRequestStorable.privateKey, + userId, + ); + } + + // Request still pending response from admin + // set keypair and create hub connection so that any approvals will be received via push notification + this.authRequestKeyPair = { privateKey: adminAuthRequestStorable.privateKey, publicKey: null }; + await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id); + } + + private async verifyAndHandleApprovedAuthReq(requestId: string): Promise { + /** + * *********************************** + * Standard Auth Request Flows + * *********************************** + * + * Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory. + * + * Unauthed user clicks "Login with device" > navigates to /login-with-device which creates a StandardAuthRequest + * > receives approval from a device with authRequestPublicKey(masterKey) > decrypts masterKey > decrypts userKey > proceed to vault + * + * Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory. + * + * Unauthed user clicks "Login with device" > navigates to /login-with-device which creates a StandardAuthRequest + * > receives approval from a device with authRequestPublicKey(userKey) > decrypts userKey > proceeds to vault + * + * Note: this flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could get into this flow: + * 1) An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey in memory. + * 2) The org admin... + * (2a) Changes the member decryption options from "Trusted devices" to "Master password" AND + * (2b) Turns off the "Require single sign-on authentication" policy + * 3) On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO. + * 4) The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in memory (see step 1 above). + * + * Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory. + * + * SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Approve from your other device" + * > navigates to /login-with-device which creates a StandardAuthRequest > receives approval from device with authRequestPublicKey(masterKey) + * > decrypts masterKey > decrypts userKey > establishes trust (if required) > proceeds to vault + * + * Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory. + * + * SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Approve from your other device" + * > navigates to /login-with-device which creates a StandardAuthRequest > receives approval from device with authRequestPublicKey(userKey) + * > decrypts userKey > establishes trust (if required) > proceeds to vault + * + * *********************************** + * Admin Auth Request Flow + * *********************************** + * + * Flow: Authed SSO TD user requests admin approval. + * + * SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Request admin approval" + * > navigates to /admin-approval-requested which creates an AdminAuthRequest > receives approval from device with authRequestPublicKey(userKey) + * > decrypts userKey > establishes trust (if required) > proceeds to vault + * + * Note: TDE users are required to be enrolled in admin password reset, which gives the admin access to the user's userKey. + * This is how admins are able to send over the authRequestPublicKey(userKey) to the user to allow them to unlock. + * + * + * Summary Table + * |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + * | Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory (see note 1) | + * |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + * | Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes | + * | Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no | + * | Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes | + * | Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no | | + * | Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey | + * |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + * * Note 1: The phrase "in memory" here is important. It is possible for a user to have a master password for their account, but not have a masterKey IN MEMORY for + * a specific device. For example, if a user registers an account with a master password, then joins an SSO TD org, then logs in to a device via SSO and + * admin auth request, they are now logged into that device but that device does not have masterKey IN MEMORY. + */ + + try { + const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked; + + if (userHasAuthenticatedViaSSO) { + // Get the auth request from the server + // User is authenticated, therefore the endpoint does not require an access code. + const authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId); + + if (authRequestResponse.requestApproved) { + // Handles Standard Flows 3-4 and Admin Flow + await this.handleAuthenticatedFlows(authRequestResponse); + } + } else { + // Get the auth request from the server + // User is unauthenticated, therefore the endpoint requires an access code for user verification. + const authRequestResponse = await this.authRequestApiService.getAuthResponse( + requestId, + this.authRequest.accessCode, + ); + + if (authRequestResponse.requestApproved) { + // Handles Standard Flows 1-2 + await this.handleUnauthenticatedFlows(authRequestResponse, requestId); + } + } + } catch (error) { + if (error instanceof ErrorResponse) { + await this.router.navigate([this.backToRoute]); + this.validationService.showError(error); + return; + } + + this.logService.error(error); + } + } + + private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) { + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + + await this.decryptViaApprovedAuthRequest( + authRequestResponse, + this.authRequestKeyPair.privateKey, + userId, + ); + } + + private async handleUnauthenticatedFlows( + authRequestResponse: AuthRequestResponse, + requestId: string, + ) { + const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials( + requestId, + authRequestResponse, + ); + + // Note: keys are set by AuthRequestLoginStrategy success handling + const authResult = await this.loginStrategyService.logIn(authRequestLoginCredentials); + + await this.handlePostLoginNavigation(authResult); + } + + private async decryptViaApprovedAuthRequest( + authRequestResponse: AuthRequestResponse, + privateKey: ArrayBuffer, + userId: UserId, + ): Promise { + /** + * See verifyAndHandleApprovedAuthReq() for flow details. + * + * We determine the type of `key` based on the presence or absence of `masterPasswordHash`: + * - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)] + * - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey) + */ + + if (authRequestResponse.masterPasswordHash) { + // ...in Standard Auth Request Flow 3 + await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash( + authRequestResponse, + privateKey, + userId, + ); + } else { + // ...in Standard Auth Request Flow 4 or Admin Auth Request Flow + await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey( + authRequestResponse, + privateKey, + userId, + ); + } + + // clear the admin auth request from state so it cannot be used again (it's a one time use) + // TODO: this should eventually be enforced via deleting this on the server once it is used + await this.authRequestService.clearAdminAuthRequest(userId); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("loginApproved"), + }); + + // Now that we have a decrypted user key in memory, we can check if we + // need to establish trust on the current device + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); + + await this.handleSuccessfulLoginNavigation(); + } + + /** + * Takes an `AuthRequestResponse` and decrypts the `key` to build an `AuthRequestLoginCredentials` + * object for use in the `AuthRequestLoginStrategy`. + * + * The credentials object that gets built is affected by whether the `authRequestResponse.key` + * is an encrypted MasterKey or an encrypted UserKey. + */ + private async buildAuthRequestLoginCredentials( + requestId: string, + authRequestResponse: AuthRequestResponse, + ): Promise { + /** + * See verifyAndHandleApprovedAuthReq() for flow details. + * + * We determine the type of `key` based on the presence or absence of `masterPasswordHash`: + * - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)] + * - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey) + */ + + if (authRequestResponse.masterPasswordHash) { + // ...in Standard Auth Request Flow 1 + const { masterKey, masterKeyHash } = + await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash( + authRequestResponse.key, + authRequestResponse.masterPasswordHash, + this.authRequestKeyPair.privateKey, + ); + + return new AuthRequestLoginCredentials( + this.email, + this.authRequest.accessCode, + requestId, + null, // no userKey + masterKey, + masterKeyHash, + ); + } else { + // ...in Standard Auth Request Flow 2 + const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey( + authRequestResponse.key, + this.authRequestKeyPair.privateKey, + ); + return new AuthRequestLoginCredentials( + this.email, + this.authRequest.accessCode, + requestId, + userKey, + null, // no masterKey + null, // no masterKeyHash + ); + } + } + + private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) { + // clear the admin auth request from state + await this.authRequestService.clearAdminAuthRequest(userId); + + // start new auth request + await this.startAdminAuthRequestLogin(); + } + + private async handlePostLoginNavigation(loginResponse: AuthResult) { + if (loginResponse.requiresTwoFactor) { + await this.router.navigate(["2fa"]); + } else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) { + await this.router.navigate(["update-temp-password"]); + } else { + await this.handleSuccessfulLoginNavigation(); + } + } + + private async handleSuccessfulLoginNavigation() { + if (this.flow === Flow.StandardAuthRequest) { + // Only need to set remembered email on standard login with auth req flow + await this.loginEmailService.saveEmailSettings(); + } + + await this.syncService.fullSync(true); + await this.router.navigate(["vault"]); + } +} diff --git a/libs/auth/src/common/abstractions/auth-request-api.service.ts b/libs/auth/src/common/abstractions/auth-request-api.service.ts new file mode 100644 index 0000000000..1b0befc0df --- /dev/null +++ b/libs/auth/src/common/abstractions/auth-request-api.service.ts @@ -0,0 +1,37 @@ +import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; + +export abstract class AuthRequestApiService { + /** + * Gets an auth request by its ID. + * + * @param requestId The ID of the auth request. + * @returns A promise that resolves to the auth request response. + */ + abstract getAuthRequest: (requestId: string) => Promise; + + /** + * Gets an auth request response by its ID and access code. + * + * @param requestId The ID of the auth request. + * @param accessCode The access code of the auth request. + * @returns A promise that resolves to the auth request response. + */ + abstract getAuthResponse: (requestId: string, accessCode: string) => Promise; + + /** + * Sends an admin auth request. + * + * @param request The auth request object. + * @returns A promise that resolves to the auth request response. + */ + abstract postAdminAuthRequest: (request: AuthRequest) => Promise; + + /** + * Sends an auth request. + * + * @param request The auth request object. + * @returns A promise that resolves to the auth request response. + */ + abstract postAuthRequest: (request: AuthRequest) => Promise; +} diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index e686de5201..093d703b74 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -1,3 +1,4 @@ +export * from "./auth-request-api.service"; export * from "./pin.service.abstraction"; export * from "./login-email.service"; export * from "./login-strategy.service"; diff --git a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts new file mode 100644 index 0000000000..180e007939 --- /dev/null +++ b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts @@ -0,0 +1,65 @@ +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { AuthRequestApiService } from "../../abstractions/auth-request-api.service"; + +export class DefaultAuthRequestApiService implements AuthRequestApiService { + constructor( + private apiService: ApiService, + private logService: LogService, + ) {} + + async getAuthRequest(requestId: string): Promise { + try { + const path = `/auth-requests/${requestId}`; + const response = await this.apiService.send("GET", path, null, true, true); + + return response; + } catch (e: unknown) { + this.logService.error(e); + throw e; + } + } + + async getAuthResponse(requestId: string, accessCode: string): Promise { + try { + const path = `/auth-requests/${requestId}/response?code=${accessCode}`; + const response = await this.apiService.send("GET", path, null, false, true); + + return response; + } catch (e: unknown) { + this.logService.error(e); + throw e; + } + } + + async postAdminAuthRequest(request: AuthRequest): Promise { + try { + const response = await this.apiService.send( + "POST", + "/auth-requests/admin-request", + request, + true, + true, + ); + + return response; + } catch (e: unknown) { + this.logService.error(e); + throw e; + } + } + + async postAuthRequest(request: AuthRequest): Promise { + try { + const response = await this.apiService.send("POST", "/auth-requests/", request, false, true); + + return response; + } catch (e: unknown) { + this.logService.error(e); + throw e; + } + } +} diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index 3a8df05779..41e0ba087a 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -3,5 +3,6 @@ export * from "./login-email/login-email.service"; export * from "./login-strategies/login-strategy.service"; export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; +export * from "./auth-request/auth-request-api.service"; export * from "./register-route.service"; export * from "./accounts/lock.service"; diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 236599ed69..b292ffdb81 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -36,7 +36,7 @@ import { ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; -import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; +import { AuthRequest } from "../auth/models/request/auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; @@ -186,8 +186,8 @@ export abstract class ApiService { putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise; postConvertToKeyConnector: () => Promise; //passwordless - postAuthRequest: (request: CreateAuthRequest) => Promise; - postAdminAuthRequest: (request: CreateAuthRequest) => Promise; + postAuthRequest: (request: AuthRequest) => Promise; + postAdminAuthRequest: (request: AuthRequest) => Promise; getAuthResponse: (id: string, accessCode: string) => Promise; getAuthRequest: (id: string) => Promise; putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise; diff --git a/libs/common/src/auth/models/request/create-auth.request.ts b/libs/common/src/auth/models/request/auth.request.ts similarity index 88% rename from libs/common/src/auth/models/request/create-auth.request.ts rename to libs/common/src/auth/models/request/auth.request.ts index ab0c512080..c992af6c1f 100644 --- a/libs/common/src/auth/models/request/create-auth.request.ts +++ b/libs/common/src/auth/models/request/auth.request.ts @@ -1,6 +1,6 @@ import { AuthRequestType } from "../../enums/auth-request-type"; -export class CreateAuthRequest { +export class AuthRequest { constructor( readonly email: string, readonly deviceIdentifier: string, diff --git a/libs/common/src/auth/models/response/auth-request.response.ts b/libs/common/src/auth/models/response/auth-request.response.ts index e37b9013f8..d0c5d66306 100644 --- a/libs/common/src/auth/models/response/auth-request.response.ts +++ b/libs/common/src/auth/models/response/auth-request.response.ts @@ -8,8 +8,8 @@ export class AuthRequestResponse extends BaseResponse { publicKey: string; requestDeviceType: DeviceType; requestIpAddress: string; - key: string; - masterPasswordHash: string; + key: string; // could be either an encrypted MasterKey or an encrypted UserKey + masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey) creationDate: string; requestApproved?: boolean; responseDate?: string; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 293aa8aa90..13b6609893 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -42,7 +42,7 @@ import { } from "../admin-console/models/response/provider/provider-user.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { TokenService } from "../auth/abstractions/token.service"; -import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; +import { AuthRequest } from "../auth/models/request/auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request"; import { EmailTokenRequest } from "../auth/models/request/email-token.request"; @@ -260,11 +260,12 @@ export class ApiService implements ApiServiceAbstraction { } // TODO: PM-3519: Create and move to AuthRequest Api service - async postAuthRequest(request: CreateAuthRequest): Promise { + // TODO: PM-9724: Remove legacy auth request methods when we remove legacy LoginViaAuthRequestV1Components + async postAuthRequest(request: AuthRequest): Promise { const r = await this.send("POST", "/auth-requests/", request, false, true); return new AuthRequestResponse(r); } - async postAdminAuthRequest(request: CreateAuthRequest): Promise { + async postAdminAuthRequest(request: AuthRequest): Promise { const r = await this.send("POST", "/auth-requests/admin-request", request, true, true); return new AuthRequestResponse(r); }