mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-24 12:06:15 +01:00
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.
This commit is contained in:
parent
2df8643e29
commit
9429ae1d06
@ -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"
|
||||
},
|
||||
|
@ -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,
|
@ -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),
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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 {}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -45,6 +45,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
@ -0,0 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-via-auth-request",
|
||||
templateUrl: "login-via-auth-request-v1.component.html",
|
||||
})
|
||||
export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 {}
|
@ -1,9 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-via-auth-request",
|
||||
templateUrl: "login-via-auth-request.component.html",
|
||||
})
|
||||
export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {}
|
@ -6,20 +6,20 @@ import { SharedModule } from "../../../app/shared";
|
||||
|
||||
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";
|
||||
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CheckboxModule],
|
||||
declarations: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponent,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
exports: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponent,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
|
@ -27,6 +27,8 @@ import {
|
||||
LockV2Component,
|
||||
LockIcon,
|
||||
UserLockIcon,
|
||||
LoginViaAuthRequestComponent,
|
||||
DevicesIcon,
|
||||
RegistrationUserAddIcon,
|
||||
RegistrationLockAltIcon,
|
||||
RegistrationExpiredLinkIcon,
|
||||
@ -46,7 +48,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 { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||
@ -96,21 +98,11 @@ const routes: Routes = [
|
||||
children: [], // Children lets us have an empty component.
|
||||
canActivate: [redirectGuard()], // Redirects either to vault, login, or lock page.
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
component: LoginViaAuthRequestComponent,
|
||||
data: { titleId: "loginWithDevice" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-with-passkey",
|
||||
component: LoginViaWebAuthnComponent,
|
||||
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
component: LoginViaAuthRequestComponent,
|
||||
data: { titleId: "adminApprovalRequested" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
component: LoginDecryptionOptionsComponent,
|
||||
@ -179,6 +171,57 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: { titleId: "loginWithDevice" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "loginInitiated",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
titleId: "loginInitiated",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: { titleId: "adminApprovalRequested" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
titleId: "adminApprovalRequested",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
|
@ -984,6 +984,9 @@
|
||||
"loginWithDeviceEnabledNote": {
|
||||
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Need another option?"
|
||||
},
|
||||
"loginWithMasterPassword": {
|
||||
"message": "Log in with master password"
|
||||
},
|
||||
@ -1302,6 +1305,12 @@
|
||||
"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"
|
||||
},
|
||||
"versionNumber": {
|
||||
"message": "Version $VERSION_NUMBER$",
|
||||
"placeholders": {
|
||||
@ -3387,6 +3396,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "View all log in options"
|
||||
},
|
||||
@ -4443,6 +4455,9 @@
|
||||
"message": "Never prompt to verify fingerprint phrases for invited users (not recommended)",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
},
|
||||
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||
"message": "You will be notified once the request is approved"
|
||||
},
|
||||
"free": {
|
||||
"message": "Free",
|
||||
"description": "Free, as in 'Free beer'"
|
||||
|
@ -18,7 +18,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
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 { CreateAuthRequest } from "@bitwarden/common/auth/models/request/create-auth.request";
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
@ -43,7 +43,7 @@ enum State {
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class LoginViaAuthRequestComponent
|
||||
export class LoginViaAuthRequestComponentV1
|
||||
extends CaptchaProtectedComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
@ -51,7 +51,7 @@ export class LoginViaAuthRequestComponent
|
||||
userAuthNStatus: AuthenticationStatus;
|
||||
email: string;
|
||||
showResendNotification = false;
|
||||
authRequest: CreateAuthRequest;
|
||||
authRequest: AuthRequest;
|
||||
fingerprintPhrase: string;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
@ -265,7 +265,7 @@ export class LoginViaAuthRequestComponent
|
||||
this.authRequestKeyPair.publicKey,
|
||||
);
|
||||
|
||||
this.authRequest = new CreateAuthRequest(
|
||||
this.authRequest = new AuthRequest(
|
||||
this.email,
|
||||
deviceIdentifier,
|
||||
publicKey,
|
@ -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({
|
||||
|
52
libs/auth/src/angular/icons/devices.icon.ts
Normal file
52
libs/auth/src/angular/icons/devices.icon.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const DevicesIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 120 100">
|
||||
<path
|
||||
class="tw-fill-art-primary"
|
||||
fill-rule="evenodd"
|
||||
d="M41.212 87.309c0-.335.271-.606.606-.606H76.97a.606.606 0 0 1 0 1.212H41.818a.606.606 0 0 1-.606-.606Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-primary"
|
||||
fill-rule="evenodd"
|
||||
d="M53.176 87.31V76.542h1.212V87.31h-1.212Zm12.103 0V76.542h1.212V87.31h-1.212Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-primary"
|
||||
fill-rule="evenodd"
|
||||
d="M16.363 29.733a8.485 8.485 0 0 1 8.485-8.485h70.303a8.485 8.485 0 0 1 8.485 8.485v3.637h-2.424v-3.637a6.06 6.06 0 0 0-6.06-6.06H24.847a6.06 6.06 0 0 0-6.06 6.06v9.697h-2.425v-9.697Zm9.091 44.849H76.97v2.424H25.454v-2.424Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-accent"
|
||||
fill-rule="evenodd"
|
||||
d="M21.212 30.34c0-2.344 1.9-4.243 4.242-4.243h69.091c2.343 0 4.243 1.9 4.243 4.242v3.03h-1.212v-3.03a3.03 3.03 0 0 0-3.03-3.03H25.453a3.03 3.03 0 0 0-3.03 3.03v9.091h-1.212v-9.09Zm4.242 40.605H76.97v1.212H25.454v-1.212Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-primary"
|
||||
fill-rule="evenodd"
|
||||
d="M75.758 38.218a6.06 6.06 0 0 1 6.06-6.06h32.122a6.06 6.06 0 0 1 6.06 6.06v48.485a6.06 6.06 0 0 1-6.06 6.06H81.818a6.06 6.06 0 0 1-6.06-6.06V38.218Zm6.06-3.636a3.636 3.636 0 0 0-3.636 3.636v48.485a3.636 3.636 0 0 0 3.636 3.636h32.122a3.636 3.636 0 0 0 3.636-3.636V38.218a3.636 3.636 0 0 0-3.636-3.636H81.818Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-accent"
|
||||
d="M99.394 87.31a1.212 1.212 0 1 1-2.424 0 1.212 1.212 0 0 1 2.424 0Z"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-primary"
|
||||
fill-rule="evenodd"
|
||||
d="M20.606 40.642H6.061a3.636 3.636 0 0 0-3.637 3.636V80.64a3.636 3.636 0 0 0 3.637 3.637h14.545a3.636 3.636 0 0 0 3.636-3.637V44.278a3.636 3.636 0 0 0-3.636-3.636ZM6.061 38.217A6.06 6.06 0 0 0 0 44.277v36.364a6.06 6.06 0 0 0 6.06 6.061h14.546a6.06 6.06 0 0 0 6.06-6.06V44.277a6.06 6.06 0 0 0-6.06-6.06H6.061Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
class="tw-fill-art-accent"
|
||||
fill-rule="evenodd"
|
||||
d="M12.345 43.556c0-.334.272-.606.606-.606h.753a.606.606 0 1 1 0 1.212h-.753a.606.606 0 0 1-.606-.606Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
<div class="tw-text-center">
|
||||
<ng-container *ngIf="flow === Flow.StandardAuthRequest">
|
||||
<p>{{ "makeSureYourAccountIsUnlockedAndTheFingerprintEtc" | i18n }}</p>
|
||||
|
||||
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
|
||||
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
|
||||
|
||||
<button
|
||||
*ngIf="showResendNotification"
|
||||
type="button"
|
||||
bitButton
|
||||
block
|
||||
buttonType="secondary"
|
||||
class="tw-mt-4"
|
||||
(click)="startStandardAuthRequestLogin()"
|
||||
>
|
||||
{{ "resendNotification" | i18n }}
|
||||
</button>
|
||||
|
||||
<div *ngIf="clientType !== ClientType.Browser" class="tw-mt-4">
|
||||
<span>{{ "needAnotherOptionV1" | i18n }}</span>
|
||||
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
|
||||
"viewAllLogInOptions" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="flow === Flow.AdminAuthRequest">
|
||||
<p>{{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}</p>
|
||||
|
||||
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
|
||||
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
|
||||
|
||||
<div class="tw-mt-4">
|
||||
<span>{{ "troubleLoggingIn" | i18n }}</span>
|
||||
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
|
||||
"viewAllLogInOptions" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
@ -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<void> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("userEmailMissing"),
|
||||
});
|
||||
|
||||
await this.router.navigate([this.backToRoute]);
|
||||
}
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
await this.anonymousHubService.stopHubConnection();
|
||||
}
|
||||
|
||||
private async startAdminAuthRequestLogin(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
/**
|
||||
* ***********************************
|
||||
* 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<void> {
|
||||
/**
|
||||
* 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<AuthRequestLoginCredentials> {
|
||||
/**
|
||||
* 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"]);
|
||||
}
|
||||
}
|
@ -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<AuthRequestResponse>;
|
||||
|
||||
/**
|
||||
* 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<AuthRequestResponse>;
|
||||
|
||||
/**
|
||||
* 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<AuthRequestResponse>;
|
||||
|
||||
/**
|
||||
* 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<AuthRequestResponse>;
|
||||
}
|
@ -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";
|
||||
|
@ -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<AuthRequestResponse> {
|
||||
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<AuthRequestResponse> {
|
||||
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<AuthRequestResponse> {
|
||||
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<AuthRequestResponse> {
|
||||
try {
|
||||
const response = await this.apiService.send("POST", "/auth-requests/", request, false, true);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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<any>;
|
||||
postConvertToKeyConnector: () => Promise<void>;
|
||||
//passwordless
|
||||
postAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AuthRequestType } from "../../enums/auth-request-type";
|
||||
|
||||
export class CreateAuthRequest {
|
||||
export class AuthRequest {
|
||||
constructor(
|
||||
readonly email: string,
|
||||
readonly deviceIdentifier: string,
|
@ -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;
|
||||
|
@ -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<AuthRequestResponse> {
|
||||
// TODO: PM-9724: Remove legacy auth request methods when we remove legacy LoginViaAuthRequestV1Components
|
||||
async postAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
|
||||
const r = await this.send("POST", "/auth-requests/", request, false, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
async postAdminAuthRequest(request: CreateAuthRequest): Promise<AuthRequestResponse> {
|
||||
async postAdminAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
|
||||
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user