diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index df4b9a9001c1..26fe9186cfef 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 9dc0d7d5454d..66c69d0a41a0 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 90990ea832dc..061067c717e3 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 d53e51e9df25..ba8ab1e7aaf3 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 2d0ccd1d1c02..d6e46de6ba0a 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 f5023cb4249c..c1e4fd18692c 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 8459dc7441e3..b59e07783715 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 c0b330bf2dd9..20c0bc97c6c9 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 78358bc00992..e9f26d23e947 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 0b3f09afc41f..69777950a780 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 @@
{{ "fingerprintPhraseHeader" | i18n }}
+
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 340e8f567cb6..251223d1befa 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 000000000000..54acea5b0877
--- /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 70460a7aea42..9c444df5702f 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 d3d9e6009181..5c028065c62d 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 000000000000..a1d0f200c151
--- /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 }}
+
+
+
+
+
+
+
+ {{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}
+
+ {{ "fingerprintPhraseHeader" | i18n }}
+ {{ fingerprintPhrase }}
+
+
+
+
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 000000000000..38614a9046a0
--- /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 000000000000..1b0befc0df41
--- /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 e686de52013d..093d703b74a5 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 000000000000..180e0079396a
--- /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 3a8df0577966..41e0ba087ae5 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 236599ed690f..b292ffdb81f7 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 ab0c512080d6..c992af6c1f8b 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 e37b9013f86c..d0c5d6630656 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 293aa8aa9017..13b660989332 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);
}