mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +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": {
|
"resendNotification": {
|
||||||
"message": "Resend notification"
|
"message": "Resend notification"
|
||||||
},
|
},
|
||||||
|
"viewAllLogInOptions": {
|
||||||
|
"message": "View all log in options"
|
||||||
|
},
|
||||||
"viewAllLoginOptions": {
|
"viewAllLoginOptions": {
|
||||||
"message": "View all log in options"
|
"message": "View all log in options"
|
||||||
},
|
},
|
||||||
"notificationSentDevice": {
|
"notificationSentDevice": {
|
||||||
"message": "A notification has been sent to your device."
|
"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": {
|
"loginInitiated": {
|
||||||
"message": "Login initiated"
|
"message": "Login initiated"
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ import { Location } from "@angular/common";
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 {
|
import {
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
@ -27,9 +27,9 @@ import { KeyService } from "@bitwarden/key-management";
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login-via-auth-request",
|
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(
|
constructor(
|
||||||
router: Router,
|
router: Router,
|
||||||
keyService: KeyService,
|
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 => tabs, login-with-device => 2fa", inSlideLeft),
|
||||||
transition("login-with-device => login", outSlideRight),
|
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(tabsToCiphers, inSlideLeft),
|
||||||
transition(ciphersToTabs, outSlideRight),
|
transition(ciphersToTabs, outSlideRight),
|
||||||
|
|
||||||
|
@ -21,10 +21,12 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh
|
|||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
|
DevicesIcon,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
LockV2Component,
|
LockV2Component,
|
||||||
|
LoginViaAuthRequestComponent,
|
||||||
PasswordHintComponent,
|
PasswordHintComponent,
|
||||||
RegistrationFinishComponent,
|
RegistrationFinishComponent,
|
||||||
RegistrationLockAltIcon,
|
RegistrationLockAltIcon,
|
||||||
@ -51,7 +53,7 @@ import { HomeComponent } from "../auth/popup/home.component";
|
|||||||
import { LockComponent } from "../auth/popup/lock.component";
|
import { LockComponent } from "../auth/popup/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
||||||
import { LoginComponentV1 } from "../auth/popup/login-v1.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 { RegisterComponent } from "../auth/popup/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
@ -171,18 +173,6 @@ const routes: Routes = [
|
|||||||
canActivate: [fido2AuthGuard],
|
canActivate: [fido2AuthGuard],
|
||||||
data: { state: "fido2" } satisfies RouteDataProperties,
|
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",
|
path: "lock",
|
||||||
component: LockComponent,
|
component: LockComponent,
|
||||||
@ -409,6 +399,61 @@ const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "update-temp-password" } satisfies RouteDataProperties,
|
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(
|
...unauthUiRefreshSwap(
|
||||||
HintComponent,
|
HintComponent,
|
||||||
ExtensionAnonLayoutWrapperComponent,
|
ExtensionAnonLayoutWrapperComponent,
|
||||||
|
@ -26,7 +26,7 @@ import { HomeComponent } from "../auth/popup/home.component";
|
|||||||
import { LockComponent } from "../auth/popup/lock.component";
|
import { LockComponent } from "../auth/popup/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
||||||
import { LoginComponentV1 } from "../auth/popup/login-v1.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 { RegisterComponent } from "../auth/popup/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
@ -159,8 +159,8 @@ import "../platform/popup/locales";
|
|||||||
HintComponent,
|
HintComponent,
|
||||||
HomeComponent,
|
HomeComponent,
|
||||||
LockComponent,
|
LockComponent,
|
||||||
|
LoginViaAuthRequestComponentV1,
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponent,
|
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
NotificationsSettingsV1Component,
|
NotificationsSettingsV1Component,
|
||||||
AppearanceComponent,
|
AppearanceComponent,
|
||||||
|
@ -18,10 +18,12 @@ import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-ref
|
|||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperData,
|
AnonLayoutWrapperData,
|
||||||
|
DevicesIcon,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
LockV2Component,
|
LockV2Component,
|
||||||
|
LoginViaAuthRequestComponent,
|
||||||
PasswordHintComponent,
|
PasswordHintComponent,
|
||||||
RegistrationFinishComponent,
|
RegistrationFinishComponent,
|
||||||
RegistrationLockAltIcon,
|
RegistrationLockAltIcon,
|
||||||
@ -42,7 +44,7 @@ import { HintComponent } from "../auth/hint.component";
|
|||||||
import { LockComponent } from "../auth/lock.component";
|
import { LockComponent } from "../auth/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
||||||
import { LoginComponentV1 } from "../auth/login/login-v1.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 { RegisterComponent } from "../auth/register.component";
|
||||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||||
@ -75,14 +77,6 @@ const routes: Routes = [
|
|||||||
canActivate: [lockGuard()],
|
canActivate: [lockGuard()],
|
||||||
canMatch: [extensionRefreshRedirect("/lockV2")],
|
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "login-with-device",
|
|
||||||
component: LoginViaAuthRequestComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "admin-approval-requested",
|
|
||||||
component: LoginViaAuthRequestComponent,
|
|
||||||
},
|
|
||||||
...twofactorRefactorSwap(
|
...twofactorRefactorSwap(
|
||||||
TwoFactorComponent,
|
TwoFactorComponent,
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
@ -130,6 +124,53 @@ const routes: Routes = [
|
|||||||
component: RemovePasswordComponent,
|
component: RemovePasswordComponent,
|
||||||
canActivate: [authGuard],
|
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(
|
...unauthUiRefreshSwap(
|
||||||
HintComponent,
|
HintComponent,
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
|
@ -2,7 +2,7 @@ import { Location } from "@angular/common";
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import {
|
import {
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
@ -30,9 +30,9 @@ import { EnvironmentComponent } from "../environment.component";
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login-via-auth-request",
|
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 })
|
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||||
environmentModal: ViewContainerRef;
|
environmentModal: ViewContainerRef;
|
||||||
showingModal = false;
|
showingModal = false;
|
@ -7,16 +7,16 @@ import { SharedModule } from "../../app/shared/shared.module";
|
|||||||
|
|
||||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
||||||
import { LoginComponentV1 } from "./login-v1.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({
|
@NgModule({
|
||||||
imports: [SharedModule, RouterModule],
|
imports: [SharedModule, RouterModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponentV1,
|
||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
],
|
],
|
||||||
exports: [LoginComponentV1, LoginViaAuthRequestComponent],
|
exports: [LoginComponentV1, LoginViaAuthRequestComponentV1],
|
||||||
})
|
})
|
||||||
export class LoginModule {}
|
export class LoginModule {}
|
||||||
|
@ -2689,15 +2689,30 @@
|
|||||||
"notificationSentDevice": {
|
"notificationSentDevice": {
|
||||||
"message": "A notification has been sent to your device."
|
"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": {
|
"fingerprintMatchInfo": {
|
||||||
"message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device."
|
"message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device."
|
||||||
},
|
},
|
||||||
"fingerprintPhraseHeader": {
|
"fingerprintPhraseHeader": {
|
||||||
"message": "Fingerprint phrase"
|
"message": "Fingerprint phrase"
|
||||||
},
|
},
|
||||||
|
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||||
|
"message": "You will be notified once the request is approved"
|
||||||
|
},
|
||||||
"needAnotherOption": {
|
"needAnotherOption": {
|
||||||
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
|
"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": {
|
"viewAllLoginOptions": {
|
||||||
"message": "View all login options"
|
"message": "View all login options"
|
||||||
},
|
},
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
||||||
<div
|
<div
|
||||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
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 { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
||||||
import { LoginComponentV1 } from "./login-v1.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";
|
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, CheckboxModule],
|
imports: [SharedModule, CheckboxModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponentV1,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
LoginViaWebAuthnComponent,
|
LoginViaWebAuthnComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
LoginComponentV1,
|
LoginComponentV1,
|
||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponentV1,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
LoginViaWebAuthnComponent,
|
LoginViaWebAuthnComponent,
|
||||||
],
|
],
|
||||||
|
@ -27,6 +27,8 @@ import {
|
|||||||
LockV2Component,
|
LockV2Component,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
UserLockIcon,
|
UserLockIcon,
|
||||||
|
LoginViaAuthRequestComponent,
|
||||||
|
DevicesIcon,
|
||||||
RegistrationUserAddIcon,
|
RegistrationUserAddIcon,
|
||||||
RegistrationLockAltIcon,
|
RegistrationLockAltIcon,
|
||||||
RegistrationExpiredLinkIcon,
|
RegistrationExpiredLinkIcon,
|
||||||
@ -46,7 +48,7 @@ import { HintComponent } from "./auth/hint.component";
|
|||||||
import { LockComponent } from "./auth/lock.component";
|
import { LockComponent } from "./auth/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
|
||||||
import { LoginComponentV1 } from "./auth/login/login-v1.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 { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||||
@ -96,21 +98,11 @@ const routes: Routes = [
|
|||||||
children: [], // Children lets us have an empty component.
|
children: [], // Children lets us have an empty component.
|
||||||
canActivate: [redirectGuard()], // Redirects either to vault, login, or lock page.
|
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",
|
path: "login-with-passkey",
|
||||||
component: LoginViaWebAuthnComponent,
|
component: LoginViaWebAuthnComponent,
|
||||||
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
|
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "admin-approval-requested",
|
|
||||||
component: LoginViaAuthRequestComponent,
|
|
||||||
data: { titleId: "adminApprovalRequested" } satisfies RouteDataProperties,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "login-initiated",
|
path: "login-initiated",
|
||||||
component: LoginDecryptionOptionsComponent,
|
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(
|
...unauthUiRefreshSwap(
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
|
@ -984,6 +984,9 @@
|
|||||||
"loginWithDeviceEnabledNote": {
|
"loginWithDeviceEnabledNote": {
|
||||||
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
|
"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": {
|
"loginWithMasterPassword": {
|
||||||
"message": "Log in with master password"
|
"message": "Log in with master password"
|
||||||
},
|
},
|
||||||
@ -1302,6 +1305,12 @@
|
|||||||
"notificationSentDevice": {
|
"notificationSentDevice": {
|
||||||
"message": "A notification has been sent to your device."
|
"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": {
|
"versionNumber": {
|
||||||
"message": "Version $VERSION_NUMBER$",
|
"message": "Version $VERSION_NUMBER$",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -3387,6 +3396,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"viewAllLogInOptions": {
|
||||||
|
"message": "View all log in options"
|
||||||
|
},
|
||||||
"viewAllLoginOptions": {
|
"viewAllLoginOptions": {
|
||||||
"message": "View all log in options"
|
"message": "View all log in options"
|
||||||
},
|
},
|
||||||
@ -4443,6 +4455,9 @@
|
|||||||
"message": "Never prompt to verify fingerprint phrases for invited users (not recommended)",
|
"message": "Never prompt to verify fingerprint phrases for invited users (not recommended)",
|
||||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||||
},
|
},
|
||||||
|
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||||
|
"message": "You will be notified once the request is approved"
|
||||||
|
},
|
||||||
"free": {
|
"free": {
|
||||||
"message": "Free",
|
"message": "Free",
|
||||||
"description": "Free, as in 'Free beer'"
|
"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 { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
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 { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
@ -43,7 +43,7 @@ enum State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class LoginViaAuthRequestComponent
|
export class LoginViaAuthRequestComponentV1
|
||||||
extends CaptchaProtectedComponent
|
extends CaptchaProtectedComponent
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
@ -51,7 +51,7 @@ export class LoginViaAuthRequestComponent
|
|||||||
userAuthNStatus: AuthenticationStatus;
|
userAuthNStatus: AuthenticationStatus;
|
||||||
email: string;
|
email: string;
|
||||||
showResendNotification = false;
|
showResendNotification = false;
|
||||||
authRequest: CreateAuthRequest;
|
authRequest: AuthRequest;
|
||||||
fingerprintPhrase: string;
|
fingerprintPhrase: string;
|
||||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||||
onSuccessfulLogin: () => Promise<any>;
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
@ -265,7 +265,7 @@ export class LoginViaAuthRequestComponent
|
|||||||
this.authRequestKeyPair.publicKey,
|
this.authRequestKeyPair.publicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.authRequest = new CreateAuthRequest(
|
this.authRequest = new AuthRequest(
|
||||||
this.email,
|
this.email,
|
||||||
deviceIdentifier,
|
deviceIdentifier,
|
||||||
publicKey,
|
publicKey,
|
@ -31,6 +31,8 @@ import {
|
|||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
LogoutReason,
|
LogoutReason,
|
||||||
RegisterRouteService,
|
RegisterRouteService,
|
||||||
|
AuthRequestApiService,
|
||||||
|
DefaultAuthRequestApiService,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||||
@ -1377,6 +1379,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultCipherAuthorizationService,
|
useClass: DefaultCipherAuthorizationService,
|
||||||
deps: [CollectionService, OrganizationServiceAbstraction],
|
deps: [CollectionService, OrganizationServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: AuthRequestApiService,
|
||||||
|
useClass: DefaultAuthRequestApiService,
|
||||||
|
deps: [ApiServiceAbstraction, LogService],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@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-logo.icon";
|
||||||
export * from "./bitwarden-shield.icon";
|
export * from "./bitwarden-shield.icon";
|
||||||
|
export * from "./devices.icon";
|
||||||
export * from "./lock.icon";
|
export * from "./lock.icon";
|
||||||
export * from "./registration-check-email.icon";
|
export * from "./registration-check-email.icon";
|
||||||
export * from "./user-lock.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/login-component.service";
|
||||||
export * from "./login/default-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
|
// password callout
|
||||||
export * from "./password-callout/password-callout.component";
|
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 "./pin.service.abstraction";
|
||||||
export * from "./login-email.service";
|
export * from "./login-email.service";
|
||||||
export * from "./login-strategy.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 "./login-strategies/login-strategy.service";
|
||||||
export * from "./user-decryption-options/user-decryption-options.service";
|
export * from "./user-decryption-options/user-decryption-options.service";
|
||||||
export * from "./auth-request/auth-request.service";
|
export * from "./auth-request/auth-request.service";
|
||||||
|
export * from "./auth-request/auth-request-api.service";
|
||||||
export * from "./register-route.service";
|
export * from "./register-route.service";
|
||||||
export * from "./accounts/lock.service";
|
export * from "./accounts/lock.service";
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
ProviderUserUserDetailsResponse,
|
ProviderUserUserDetailsResponse,
|
||||||
} from "../admin-console/models/response/provider/provider-user.response";
|
} from "../admin-console/models/response/provider/provider-user.response";
|
||||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.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 { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||||
@ -186,8 +186,8 @@ export abstract class ApiService {
|
|||||||
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
|
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
|
||||||
postConvertToKeyConnector: () => Promise<void>;
|
postConvertToKeyConnector: () => Promise<void>;
|
||||||
//passwordless
|
//passwordless
|
||||||
postAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
|
postAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||||
postAdminAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
|
postAdminAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||||
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||||
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AuthRequestType } from "../../enums/auth-request-type";
|
import { AuthRequestType } from "../../enums/auth-request-type";
|
||||||
|
|
||||||
export class CreateAuthRequest {
|
export class AuthRequest {
|
||||||
constructor(
|
constructor(
|
||||||
readonly email: string,
|
readonly email: string,
|
||||||
readonly deviceIdentifier: string,
|
readonly deviceIdentifier: string,
|
@ -8,8 +8,8 @@ export class AuthRequestResponse extends BaseResponse {
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
requestDeviceType: DeviceType;
|
requestDeviceType: DeviceType;
|
||||||
requestIpAddress: string;
|
requestIpAddress: string;
|
||||||
key: string;
|
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
|
||||||
masterPasswordHash: string;
|
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
requestApproved?: boolean;
|
requestApproved?: boolean;
|
||||||
responseDate?: string;
|
responseDate?: string;
|
||||||
|
@ -42,7 +42,7 @@ import {
|
|||||||
} from "../admin-console/models/response/provider/provider-user.response";
|
} from "../admin-console/models/response/provider/provider-user.response";
|
||||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||||
import { TokenService } from "../auth/abstractions/token.service";
|
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 { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||||
import { EmailTokenRequest } from "../auth/models/request/email-token.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
|
// 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);
|
const r = await this.send("POST", "/auth-requests/", request, false, true);
|
||||||
return new AuthRequestResponse(r);
|
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);
|
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
|
||||||
return new AuthRequestResponse(r);
|
return new AuthRequestResponse(r);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user