1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

Auth/pm 1050/pm 1051/remaining tde approval flows (#5864)

This commit is contained in:
Jared Snider 2023-07-25 19:25:00 -04:00 committed by GitHub
parent 79d186f4f5
commit 0b861f4d0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1380 additions and 289 deletions

View File

@ -2270,5 +2270,26 @@
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
},
"adminApprovalRequested": {
"message": "Admin approval requested"
},
"adminApprovalRequestSentToAdmins": {
"message": "Your request has been sent to your admin."
},
"youWillBeNotifiedOnceApproved": {
"message": "You will be notified once approved."
},
"troubleLoggingIn": {
"message": "Trouble logging in?"
},
"loginApproved": {
"message": "Login approved"
},
"userEmailMissing": {
"message": "User email missing"
},
"deviceTrusted": {
"message": "Device trusted"
}
}

View File

@ -0,0 +1,29 @@
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
} from "../../../platform/background/service-factories/crypto-service.factory";
import {
CachedServices,
FactoryOptions,
factory,
} from "../../../platform/background/service-factories/factory-options";
type AuthRequestCryptoServiceFactoryOptions = FactoryOptions;
export type AuthRequestCryptoServiceInitOptions = AuthRequestCryptoServiceFactoryOptions &
CryptoServiceInitOptions;
export function authRequestCryptoServiceFactory(
cache: { authRequestCryptoService?: AuthRequestCryptoServiceAbstraction } & CachedServices,
opts: AuthRequestCryptoServiceInitOptions
): Promise<AuthRequestCryptoServiceAbstraction> {
return factory(
cache,
"authRequestCryptoService",
opts,
async () => new AuthRequestCryptoServiceImplementation(await cryptoServiceFactory(cache, opts))
);
}

View File

@ -52,7 +52,14 @@ import {
PasswordStrengthServiceInitOptions,
} from "../../../tools/background/service_factories/password-strength-service.factory";
import { deviceTrustCryptoServiceFactory } from "./device-trust-crypto-service.factory";
import {
authRequestCryptoServiceFactory,
AuthRequestCryptoServiceInitOptions,
} from "./auth-request-crypto-service.factory";
import {
deviceTrustCryptoServiceFactory,
DeviceTrustCryptoServiceInitOptions,
} from "./device-trust-crypto-service.factory";
import {
keyConnectorServiceFactory,
KeyConnectorServiceInitOptions,
@ -76,7 +83,9 @@ export type AuthServiceInitOptions = AuthServiceFactoyOptions &
I18nServiceInitOptions &
EncryptServiceInitOptions &
PolicyServiceInitOptions &
PasswordStrengthServiceInitOptions;
PasswordStrengthServiceInitOptions &
DeviceTrustCryptoServiceInitOptions &
AuthRequestCryptoServiceInitOptions;
export function authServiceFactory(
cache: { authService?: AbstractAuthService } & CachedServices,
@ -103,7 +112,8 @@ export function authServiceFactory(
await encryptServiceFactory(cache, opts),
await passwordStrengthServiceFactory(cache, opts),
await policyServiceFactory(cache, opts),
await deviceTrustCryptoServiceFactory(cache, opts)
await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestCryptoServiceFactory(cache, opts)
)
);
}

View File

@ -26,6 +26,14 @@ import {
FactoryOptions,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
I18nServiceInitOptions,
i18nServiceFactory,
} from "../../../platform/background/service-factories/i18n-service.factory";
import {
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
} from "../../../platform/background/service-factories/platform-utils-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
@ -39,7 +47,9 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor
EncryptServiceInitOptions &
StateServiceInitOptions &
AppIdServiceInitOptions &
DevicesApiServiceInitOptions;
DevicesApiServiceInitOptions &
I18nServiceInitOptions &
PlatformUtilsServiceInitOptions;
export function deviceTrustCryptoServiceFactory(
cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices,
@ -56,7 +66,9 @@ export function deviceTrustCryptoServiceFactory(
await encryptServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await appIdServiceFactory(cache, opts),
await devicesApiServiceFactory(cache, opts)
await devicesApiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts)
)
);
}

View File

@ -5,32 +5,57 @@
</h1>
</header>
<div class="content login-page">
<div>
<p class="lead">{{ "logInInitiated" | i18n }}</p>
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<div>
<p>{{ "notificationSentDevice" | i18n }}</p>
<p class="lead">{{ "logInInitiated" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
<div>
<p>{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div>
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="fingerprint-text">
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<div class="resend-notification" *ngIf="showResendNotification">
<a (click)="startPasswordlessLogin()">{{ "resendNotification" | i18n }}</a>
</div>
<div class="footer">
{{ "loginWithDeviceEnabledInfo" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</ng-container>
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
<div>
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="fingerprint-text">
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<p class="lead">{{ "adminApprovalRequested" | i18n }}</p>
<div class="resend-notification" *ngIf="showResendNotification">
<a (click)="startPasswordlessLogin()">{{ "resendNotification" | i18n }}</a>
</div>
<div>
<p>{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p>{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
<div class="footer">
{{ "loginWithDeviceEnabledInfo" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
<div>
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="fingerprint-text">
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<div class="footer">
{{ "troubleLoggingIn" | i18n }}
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</div>
</ng-container>
</div>
</div>

View File

@ -4,7 +4,9 @@ import { Router } from "@angular/router";
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -42,7 +44,9 @@ export class LoginWithDeviceComponent
validationService: ValidationService,
stateService: StateService,
loginService: LoginService,
syncService: SyncService
syncService: SyncService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {
super(
router,
@ -59,7 +63,9 @@ export class LoginWithDeviceComponent
anonymousHubService,
validationService,
stateService,
loginService
loginService,
deviceTrustCryptoService,
authReqCryptoService
);
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);

View File

@ -15,6 +15,7 @@ import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitw
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
@ -23,6 +24,7 @@ import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
@ -201,6 +203,7 @@ export default class MainBackground {
configApiService: ConfigApiServiceAbstraction;
devicesApiService: DevicesApiServiceAbstraction;
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
backgroundWindow = window;
@ -399,9 +402,13 @@ export default class MainBackground {
this.encryptService,
this.stateService,
this.appIdService,
this.devicesApiService
this.devicesApiService,
this.i18nService,
this.platformUtilsService
);
this.authRequestCryptoService = new AuthRequestCryptoServiceImplementation(this.cryptoService);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
@ -418,7 +425,8 @@ export default class MainBackground {
this.encryptService,
this.passwordStrengthService,
this.policyService,
this.deviceTrustCryptoService
this.deviceTrustCryptoService,
this.authRequestCryptoService
);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(

View File

@ -78,6 +78,12 @@ const routes: Routes = [
canActivate: [UnauthGuard],
data: { state: "login-with-device" },
},
{
path: "admin-approval-requested",
component: LoginWithDeviceComponent,
canActivate: [],
data: { state: "login-with-device" },
},
{
path: "lock",
component: LockComponent,

View File

@ -12,8 +12,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/services/or
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
@ -146,6 +148,7 @@ export class Main {
sendApiService: SendApiService;
devicesApiService: DevicesApiServiceAbstraction;
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
constructor() {
let p = null;
@ -328,9 +331,13 @@ export class Main {
this.encryptService,
this.stateService,
this.appIdService,
this.devicesApiService
this.devicesApiService,
this.i18nService,
this.platformUtilsService
);
this.authRequestCryptoService = new AuthRequestCryptoServiceImplementation(this.cryptoService);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
@ -347,7 +354,8 @@ export class Main {
this.encryptService,
this.passwordStrengthService,
this.policyService,
this.deviceTrustCryptoService
this.deviceTrustCryptoService,
this.authRequestCryptoService
);
const lockedCallback = async () =>

View File

@ -39,6 +39,10 @@ const routes: Routes = [
path: "login-with-device",
component: LoginWithDeviceComponent,
},
{
path: "admin-approval-requested",
component: LoginWithDeviceComponent,
},
{ path: "2fa", component: TwoFactorComponent },
{
path: "login-initiated",

View File

@ -1,40 +1,72 @@
<div id="login-with-device-page">
<div id="content" class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead text-center">{{ "logInInitiated" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<p class="lead text-center">{{ "logInInitiated" | i18n }}</p>
<div class="fingerprint section">
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
<code>{{ fingerprintPhrase }}</code>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div class="section" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<div class="fingerprint section">
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
<code>{{ fingerprintPhrase }}</code>
</div>
<div class="sub-options another-method">
<p class="no-margin description-text">
{{ "needAnotherOption" | i18n }}
<a type="button" class="text text-primary" (click)="goToLogin()">
{{ "viewAllLoginOptions" | i18n }}
</a>
</p>
<div class="section" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<div class="sub-options another-method">
<p class="no-margin description-text">
{{ "needAnotherOption" | i18n }}
<a type="button" class="text text-primary" (click)="goToLogin()">
{{ "viewAllLoginOptions" | i18n }}
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
<p class="lead text-center">{{ "adminApprovalRequested" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p class="section">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
<div class="fingerprint section">
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
<code>{{ fingerprintPhrase }}</code>
</div>
<div class="sub-options another-method">
<p class="no-margin description-text">
{{ "troubleLoggingIn" | i18n }}
<a type="button" class="text text-primary" (click)="goToLogin()">
{{ "viewAllLoginOptions" | i18n }}
</a>
</p>
</div>
</div>
</div>
</div>
</ng-container>
</div>
</div>
<ng-template #environment></ng-template>

View File

@ -5,7 +5,9 @@ import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwa
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -50,7 +52,9 @@ export class LoginWithDeviceComponent
private modalService: ModalService,
syncService: SyncService,
stateService: StateService,
loginService: LoginService
loginService: LoginService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {
super(
router,
@ -67,7 +71,9 @@ export class LoginWithDeviceComponent
anonymousHubService,
validationService,
stateService,
loginService
loginService,
deviceTrustCryptoService,
authReqCryptoService
);
super.onSuccessfulLogin = () => {
@ -101,6 +107,17 @@ export class LoginWithDeviceComponent
}
goToLogin() {
this.router.navigate(["/login"]);
switch (this.state) {
case this.StateEnum.StandardAuthRequest:
this.router.navigate(["/login"]);
break;
case this.StateEnum.AdminAuthRequest:
this.router.navigate(["/login-initiated"]);
break;
default:
break;
}
}
}

View File

@ -2289,5 +2289,26 @@
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
},
"adminApprovalRequested": {
"message": "Admin approval requested"
},
"adminApprovalRequestSentToAdmins": {
"message": "Your request has been sent to your admin."
},
"youWillBeNotifiedOnceApproved": {
"message": "You will be notified once approved."
},
"troubleLoggingIn": {
"message": "Trouble logging in?"
},
"loginApproved": {
"message": "Login approved"
},
"userEmailMissing": {
"message": "User email missing"
},
"deviceTrusted": {
"message": "Device trusted"
}
}

View File

@ -5,42 +5,71 @@
>
<div>
<img class="logo logo-themed" alt="Bitwarden" />
<p class="tw-mx-4 tw-mb-4 tw-mt-3 tw-text-center tw-text-xl">
{{ "loginOrCreateNewAccount" | i18n }}
</p>
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInInitiated" | i18n }}</h2>
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<p class="tw-mx-4 tw-mb-4 tw-mt-3 tw-text-center tw-text-xl">
{{ "loginOrCreateNewAccount" | i18n }}
</p>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInInitiated" | i18n }}</h2>
<p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }}
</p>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<div class="tw-my-10" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "loginWithDeviceEnabledNote" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</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"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ fingerprintPhrase }}</code>
</p>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "troubleLoggingIn" | i18n }}
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
<div class="tw-my-10" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "loginWithDeviceEnabledNote" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</ng-container>
</div>
</div>

View File

@ -4,7 +4,9 @@ import { Router } from "@angular/router";
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -41,7 +43,9 @@ export class LoginWithDeviceComponent
anonymousHubService: AnonymousHubService,
validationService: ValidationService,
stateService: StateService,
loginService: LoginService
loginService: LoginService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {
super(
router,
@ -58,7 +62,9 @@ export class LoginWithDeviceComponent
anonymousHubService,
validationService,
stateService,
loginService
loginService,
deviceTrustCryptoService,
authReqCryptoService
);
}
}

View File

@ -67,6 +67,11 @@ const routes: Routes = [
component: LoginWithDeviceComponent,
data: { titleId: "loginWithDevice" },
},
{
path: "admin-approval-requested",
component: LoginWithDeviceComponent,
data: { titleId: "loginWithDevice" },
},
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
{
path: "login-initiated",

View File

@ -7000,5 +7000,26 @@
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
},
"adminApprovalRequested": {
"message": "Admin approval requested"
},
"adminApprovalRequestSentToAdmins": {
"message": "Your request has been sent to your admin."
},
"youWillBeNotifiedOnceApproved": {
"message": "You will be notified once approved."
},
"troubleLoggingIn": {
"message": "Trouble logging in?"
},
"loginApproved": {
"message": "Login approved"
},
"userEmailMissing": {
"message": "User email missing"
},
"deviceTrusted": {
"message": "Device trusted"
}
}

View File

@ -206,31 +206,21 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
});
}
approveFromOtherDevice() {
// TODO: plan is to re-use existing login-with-device component but rework it to have two flows
// (1) Standard flow for unauthN user based on AuthService status
// (2) New flow for authN user based on AuthService status b/c they have just authenticated w/ SSO
async approveFromOtherDevice() {
if (this.data.state !== State.ExistingUserUntrustedDevice) {
return;
}
await this.deviceTrustCryptoService.setShouldTrustDevice(this.rememberDevice.value);
this.loginService.setEmail(this.data.userEmail);
this.router.navigate(["/login-with-device"]);
}
requestAdminApproval() {
// this.router.navigate(["/admin-approval-requested"]); // new component that doesn't exist yet
// Idea: extract logic from the existing login-with-device component into a base-auth-request-component that
// the new admin-approval-requested component and the existing login-with-device component can extend
// TODO: how to do:
// add create admin approval request on new OrganizationAuthRequestsController on the server
// once https://github.com/bitwarden/server/pull/2993 is merged
// Client will create an AuthRequest of type AdminAuthRequest WITHOUT orgId and send it to the server
// Server will look up the org id(s) based on the user id and create the AdminAuthRequest(s)
// Note: must lookup if the user has an account recovery key (resetPasswordKey) set in the org
// (means they've opted into the Admin Acct Recovery feature)
// Per discussion with Micah, fire out requests to all admins in any orgs the user is a member of
// UNTIL the Admin Console team finishes their work to turn on Single Org policy when Admin Acct Recovery is enabled.
async requestAdminApproval() {
await this.deviceTrustCryptoService.setShouldTrustDevice(this.rememberDevice.value);
this.loginService.setEmail(this.data.userEmail);
this.router.navigate(["/admin-approval-requested"]);
}
async approveWithMasterPassword() {

View File

@ -303,12 +303,7 @@ export class LockComponent implements OnInit, OnDestroy {
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
const shouldTrustDevice = await this.deviceTrustCryptoService.getShouldTrustDevice();
if (shouldTrustDevice) {
await this.deviceTrustCryptoService.trustDevice();
// reset the trust choice
await this.deviceTrustCryptoService.setShouldTrustDevice(false);
}
await this.deviceTrustCryptoService.trustDeviceIfRequired();
await this.doContinue(evaluatePasswordAfterUnlock);
}

View File

@ -1,12 +1,17 @@
import { Directive, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { IsActiveMatchOptions, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
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 { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { PasswordlessLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-create-auth.request";
@ -22,20 +27,24 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
// TODO: consider renaming this component something like LoginViaAuthReqComponent
enum State {
StandardAuthRequest,
AdminAuthRequest,
}
@Directive()
export class LoginWithDeviceComponent
extends CaptchaProtectedComponent
implements OnInit, OnDestroy
{
private destroy$ = new Subject<void>();
userAuthNStatus: AuthenticationStatus;
email: string;
showResendNotification = false;
passwordlessRequest: PasswordlessCreateAuthRequest;
@ -45,12 +54,19 @@ export class LoginWithDeviceComponent
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
protected adminApprovalRoute = "admin-approval-requested";
protected StateEnum = State;
protected state = State.StandardAuthRequest;
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
private resendTimeout = 12000;
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
private authRequestKeyPair: { publicKey: ArrayBuffer; privateKey: ArrayBuffer };
// TODO: in future, go to child components and remove child constructors and let deps fall through to the super class
constructor(
protected router: Router,
private cryptoService: CryptoService,
@ -66,10 +82,14 @@ export class LoginWithDeviceComponent
private anonymousHubService: AnonymousHubService,
private validationService: ValidationService,
private stateService: StateService,
private loginService: LoginService
private loginService: LoginService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {
super(environmentService, i18nService, platformUtilsService);
// TODO: I don't know why this is necessary.
// Why would the existence of the email depend on the navigation?
const navigation = this.router.getCurrentNavigation();
if (navigation) {
this.email = this.loginService.getEmail();
@ -80,25 +100,159 @@ export class LoginWithDeviceComponent
.getPushNotificationObs$()
.pipe(takeUntil(this.destroy$))
.subscribe((id) => {
this.confirmResponse(id);
// Only fires on approval currently
this.verifyAndHandleApprovedAuthReq(id);
});
}
async ngOnInit() {
if (!this.email) {
this.router.navigate(["/login"]);
return;
this.userAuthNStatus = await this.authService.getAuthStatus();
const matchOptions: IsActiveMatchOptions = {
paths: "exact",
queryParams: "ignored",
fragment: "ignored",
matrixParams: "ignored",
};
if (this.router.isActive(this.adminApprovalRoute, matchOptions)) {
this.state = State.AdminAuthRequest;
}
if (this.state === State.AdminAuthRequest) {
// Pull email from state for admin auth reqs b/c it is available
// This also prevents it from being lost on refresh as the
// login service email does not persist.
this.email = await this.stateService.getEmail();
if (!this.email) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
this.router.navigate(["/login-initiated"]);
return;
}
// We only allow a single admin approval request to be active at a time
// so must check state to see if we have an existing one or not
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
if (adminAuthReqStorable) {
await this.handleExistingAdminAuthRequest(adminAuthReqStorable);
} else {
// No existing admin auth request; so we need to create one
await this.startPasswordlessLogin();
}
} else {
// Standard auth request
// TODO: evaluate if we can remove the setting of this.email in the constructor
this.email = this.loginService.getEmail();
if (!this.email) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
this.router.navigate(["/login"]);
return;
}
await this.startPasswordlessLogin();
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.anonymousHubService.stopHubConnection();
}
private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) {
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
// has been approved and handle it if so.
// Regardless, we always retrieve the auth request from the server verify and handle status changes here as well
const adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
// Request doesn't exist
if (!adminAuthReqResponse) {
return await this.handleExistingAdminAuthReqDeletedOrDenied();
}
// 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(
adminAuthReqStorable.privateKey
);
this.fingerprintPhrase = (
await this.cryptoService.getFingerprint(this.email, derivedPublicKeyArrayBuffer)
).join("-");
// Request denied
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
return await this.handleExistingAdminAuthReqDeletedOrDenied();
}
// Request approved
if (adminAuthReqResponse.requestApproved) {
return await this.handleApprovedAdminAuthRequest(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
}
// Request still pending response from admin
// So, create hub connection so that any approvals will be received via push notification
this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
}
private async handleExistingAdminAuthReqDeletedOrDenied() {
// clear the admin auth request from state
await this.stateService.setAdminAuthRequest(null);
// start new auth request
this.startPasswordlessLogin();
}
private async buildAuthRequest(authRequestType: AuthRequestType) {
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({ length: 25 });
this.fingerprintPhrase = (
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair.publicKey)
).join("-");
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
this.email,
deviceIdentifier,
publicKey,
authRequestType,
accessCode
);
}
async startPasswordlessLogin() {
this.showResendNotification = false;
try {
await this.buildAuthRequest();
const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
let reqResponse: AuthRequestResponse;
if (this.state === State.AdminAuthRequest) {
await this.buildAuthRequest(AuthRequestType.AdminApproval);
reqResponse = await this.apiService.postAdminAuthRequest(this.passwordlessRequest);
const adminAuthReqStorable = new AdminAuthRequestStorable({
id: reqResponse.id,
privateKey: this.authRequestKeyPair.privateKey,
});
await this.stateService.setAdminAuthRequest(adminAuthReqStorable);
} else {
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
}
if (reqResponse.id) {
this.anonymousHubService.createHubConnection(reqResponse.id);
@ -112,66 +266,69 @@ export class LoginWithDeviceComponent
}, this.resendTimeout);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.anonymousHubService.stopHubConnection();
}
private async confirmResponse(requestId: string) {
private async verifyAndHandleApprovedAuthReq(requestId: string) {
try {
// TODO for TDE: We are going to have to make changes here to support the new unlock flow as the user is already AuthN via SSO
// The existing flow currently works for unauthN users and authenticates them AND unlocks their vault.
// We only need the unlock portion of the logic to run.
// Retrieve the auth request from server and verify it's approved
let authReqResponse: AuthRequestResponse;
// We need to make the approving device treats the MP hash as optional
// and make sure the server can handle that.
switch (this.state) {
case State.StandardAuthRequest:
// Unauthed - access code required for user verification
authReqResponse = await this.apiService.getAuthResponse(
requestId,
this.passwordlessRequest.accessCode
);
break;
const response = await this.apiService.getAuthResponse(
requestId,
this.passwordlessRequest.accessCode
);
case State.AdminAuthRequest:
// Authed - no access code required
authReqResponse = await this.apiService.getAuthRequest(requestId);
break;
if (!response.requestApproved) {
default:
break;
}
if (!authReqResponse.requestApproved) {
return;
}
// TODO for TDE:
// Add a check here to see if the user is already AuthN via SSO, then we
// have to figure out how to handle the unlock portion of the logic.
// Taken from PasswordlessLogInStrategy:
// await this.cryptoService.setKey(this.passwordlessCredentials.decKey);
// navigate to vault
// Approved so proceed:
const credentials = await this.buildLoginCredentials(requestId, response);
const loginResponse = await this.authService.logIn(credentials);
// 4 Scenarios to handle for approved auth requests:
// Existing flow 1:
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(masterKey)
// > decrypt masterKey > must authenticate > gets masterKey(userKey) > decrypt userKey and proceed to vault
if (loginResponse.requiresTwoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
this.onSuccessfulLoginTwoFactorNavigate();
} else {
this.router.navigate([this.twoFactorRoute]);
}
} else if (loginResponse.forcePasswordReset != ForceResetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
this.onSuccessfulLoginForceResetNavigate();
} else {
this.router.navigate([this.forcePasswordResetRoute]);
}
} else {
await this.setRememberEmailValues();
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute]);
}
// 3 new flows from TDE:
// Flow 2:
// - Post SSO > User is AuthN > SSO login strategy success sets masterKey(userKey) > receives approval from device with pubKey(masterKey)
// > decrypt masterKey > decrypt userKey > establish trust if required > proceed to vault
// Flow 3:
// - Post SSO > User is AuthN > Receives approval from device with pubKey(userKey) > decrypt userKey > establish trust if required > proceed to vault
// Flow 4:
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(userKey)
// > decrypt userKey > must authenticate > set userKey > proceed to vault
// if user has authenticated via SSO
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
return await this.handleApprovedAdminAuthRequest(
authReqResponse,
this.authRequestKeyPair.privateKey
);
}
// Flow 1 and 4:
const loginAuthResult = await this.loginViaPasswordlessStrategy(requestId, authReqResponse);
await this.handlePostLoginNavigation(loginAuthResult);
} catch (error) {
if (error instanceof ErrorResponse) {
this.router.navigate(["/login"]);
let errorRoute = "/login";
if (this.state === State.AdminAuthRequest) {
errorRoute = "/login-initiated";
}
this.router.navigate([errorRoute]);
this.validationService.showError(error);
return;
}
@ -180,53 +337,129 @@ export class LoginWithDeviceComponent
}
}
async handleApprovedAdminAuthRequest(
adminAuthReqResponse: AuthRequestResponse,
privateKey: ArrayBuffer
) {
// See verifyAndHandleApprovedAuthReq(...) for flow details
// it's flow 2 or 3 based on presence of masterPasswordHash
if (adminAuthReqResponse.masterPasswordHash) {
// Flow 2: masterPasswordHash is not null
// key is authRequestPublicKey(masterKey) + we have authRequestPublicKey(masterPasswordHash)
await this.authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse,
privateKey
);
} else {
// Flow 3: masterPasswordHash is null
// we can assume key is authRequestPublicKey(userKey) and we can just decrypt with userKey and proceed to vault
await this.authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse,
privateKey
);
}
// 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.stateService.setAdminAuthRequest(null);
this.platformUtilsService.showToast("success", null, 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
await this.deviceTrustCryptoService.trustDeviceIfRequired();
// TODO: don't forget to use auto enrollment service everywhere we trust device
await this.handleSuccessfulLoginNavigation();
}
// Authentication helper
private async buildPasswordlessLoginCredentials(
requestId: string,
response: AuthRequestResponse
): Promise<PasswordlessLogInCredentials> {
// if masterPasswordHash has a value, we will always receive key as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
// if masterPasswordHash is null, we will always receive key as authRequestPublicKey(userKey)
if (response.masterPasswordHash) {
const { masterKey, masterKeyHash } =
await this.authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash(
response.key,
response.masterPasswordHash,
this.authRequestKeyPair.privateKey
);
return new PasswordlessLogInCredentials(
this.email,
this.passwordlessRequest.accessCode,
requestId,
null, // no userKey
masterKey,
masterKeyHash
);
} else {
const userKey = await this.authReqCryptoService.decryptPubKeyEncryptedUserKey(
response.key,
this.authRequestKeyPair.privateKey
);
return new PasswordlessLogInCredentials(
this.email,
this.passwordlessRequest.accessCode,
requestId,
userKey,
null, // no masterKey
null // no masterKeyHash
);
}
}
private async loginViaPasswordlessStrategy(
requestId: string,
authReqResponse: AuthRequestResponse
): Promise<AuthResult> {
// Note: credentials change based on if the authReqResponse.key is a encryptedMasterKey or UserKey
const credentials = await this.buildPasswordlessLoginCredentials(requestId, authReqResponse);
// Note: keys are set by PasswordlessLogInStrategy success handling
return await this.authService.logIn(credentials);
}
// Routing logic
private async handlePostLoginNavigation(loginResponse: AuthResult) {
if (loginResponse.requiresTwoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
this.onSuccessfulLoginTwoFactorNavigate();
} else {
this.router.navigate([this.twoFactorRoute]);
}
} else if (loginResponse.forcePasswordReset != ForceResetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
this.onSuccessfulLoginForceResetNavigate();
} else {
this.router.navigate([this.forcePasswordResetRoute]);
}
} else {
await this.handleSuccessfulLoginNavigation();
}
}
async setRememberEmailValues() {
// TODO: solve bug with getRememberEmail not persisting across SSO to here
const rememberEmail = this.loginService.getRememberEmail();
const rememberedEmail = this.loginService.getEmail();
const rememberedEmail = this.loginService.getEmail(); // this does persist across SSO
await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null);
this.loginService.clearValues();
}
private async buildAuthRequest() {
this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const deviceIdentifier = await this.appIdService.getAppId();
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]);
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
this.fingerprintPhrase = (
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0])
).join("-");
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
this.email,
deviceIdentifier,
publicKey,
AuthRequestType.AuthenticateAndUnlock,
accessCode
);
}
private async buildLoginCredentials(
requestId: string,
response: AuthRequestResponse
): Promise<PasswordlessLogInCredentials> {
const decMasterKeyArray = await this.cryptoService.rsaDecrypt(
response.key,
this.authRequestKeyPair[1]
);
const decMasterPasswordHash = await this.cryptoService.rsaDecrypt(
response.masterPasswordHash,
this.authRequestKeyPair[1]
);
const decMasterKey = new SymmetricCryptoKey(decMasterKeyArray) as MasterKey;
const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash);
return new PasswordlessLogInCredentials(
this.email,
this.passwordlessRequest.accessCode,
requestId,
decMasterKey,
localHashedPassword
);
private async handleSuccessfulLoginNavigation() {
await this.setRememberEmailValues();
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute]);
}
}
}

View File

@ -232,8 +232,6 @@ export class SsoComponent {
return await this.handleSuccessfulLogin();
} catch (e) {
await this.handleLoginError(e);
} finally {
this.loggingIn = false;
}
}

View File

@ -40,6 +40,7 @@ import {
AccountService as AccountServiceAbstraction,
InternalAccountService,
} from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
@ -52,6 +53,7 @@ import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/ab
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
@ -250,6 +252,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
PasswordStrengthServiceAbstraction,
PolicyServiceAbstraction,
DeviceTrustCryptoServiceAbstraction,
AuthRequestCryptoServiceAbstraction,
],
},
{
@ -708,8 +711,15 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
StateServiceAbstraction,
AppIdServiceAbstraction,
DevicesApiServiceAbstraction,
I18nServiceAbstraction,
PlatformUtilsServiceAbstraction,
],
},
{
provide: AuthRequestCryptoServiceAbstraction,
useClass: AuthRequestCryptoServiceImplementation,
deps: [CryptoServiceAbstraction],
},
],
})
export class JslibServicesModule {}

View File

@ -200,6 +200,7 @@ export abstract class ApiService {
postConvertToKeyConnector: () => Promise<void>;
//passwordless
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
postAdminAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;

View File

@ -0,0 +1,24 @@
import { UserKey, MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestResponse } from "../models/response/auth-request.response";
export abstract class AuthRequestCryptoServiceAbstraction {
setUserKeyAfterDecryptingSharedUserKey: (
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) => Promise<void>;
setKeysAfterDecryptingSharedMasterKeyAndHash: (
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) => Promise<void>;
decryptPubKeyEncryptedUserKey: (
pubKeyEncryptedUserKey: string,
privateKey: ArrayBuffer
) => Promise<UserKey>;
decryptPubKeyEncryptedMasterKeyAndHash: (
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: ArrayBuffer
) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
}

View File

@ -10,6 +10,8 @@ export abstract class DeviceTrustCryptoServiceAbstraction {
getShouldTrustDevice: () => Promise<boolean>;
setShouldTrustDevice: (value: boolean) => Promise<void>;
trustDeviceIfRequired: () => Promise<void>;
trustDevice: () => Promise<DeviceResponse>;
getDeviceKey: () => Promise<DeviceKey>;
decryptUserKeyWithDeviceKey: (

View File

@ -1,6 +1,11 @@
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
export abstract class PasswordResetEnrollmentServiceAbstraction {
/*
* Checks the user's enrollment status and enrolls them if required
*/
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
/**
* Enroll current user in password reset
* @param organizationId - Organization in which to enroll the user

View File

@ -1,4 +1,5 @@
export enum AuthRequestType {
AuthenticateAndUnlock = 0,
Unlock = 1,
AdminApproval = 2,
}

View File

@ -117,6 +117,9 @@ export abstract class LogInStrategy {
accountKeys.deviceKey = deviceKey;
}
// If you don't persist existing admin auth requests on login, they will get deleted.
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
await this.stateService.addAccount(
new Account({
profile: {
@ -143,6 +146,7 @@ export abstract class LogInStrategy {
decryptionOptions: AccountDecryptionOptions.fromResponse(
tokenResponse.userDecryptionOptions
),
adminAuthRequest: adminAuthRequest,
})
);
}

View File

@ -14,6 +14,7 @@ import {
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
@ -22,7 +23,7 @@ import { IdentityTokenResponse } from "../models/response/identity-token.respons
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { PasswordlessLogInStrategy } from "./passwordless-login.strategy";
describe("SsoLogInStrategy", () => {
describe("PasswordlessLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
@ -32,6 +33,7 @@ describe("SsoLogInStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let passwordlessLoginStrategy: PasswordlessLogInStrategy;
let credentials: PasswordlessLogInCredentials;
@ -42,8 +44,11 @@ describe("SsoLogInStrategy", () => {
const email = "EMAIL";
const accessCode = "ACCESS_CODE";
const authRequestId = "AUTH_REQUEST_ID";
const decKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const localPasswordHash = "LOCAL_PASSWORD_HASH";
const decMasterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
const decUserKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const decMasterKeyHash = "LOCAL_PASSWORD_HASH";
beforeEach(async () => {
cryptoService = mock<CryptoService>();
@ -55,6 +60,7 @@ describe("SsoLogInStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@ -69,21 +75,24 @@ describe("SsoLogInStrategy", () => {
messagingService,
logService,
stateService,
twoFactorService
);
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
decKey,
localPasswordHash
twoFactorService,
deviceTrustCryptoService
);
tokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
});
it("sets keys after a successful authentication", async () => {
it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => {
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
null,
decMasterKey,
decMasterKeyHash
);
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
@ -93,9 +102,37 @@ describe("SsoLogInStrategy", () => {
await passwordlessLoginStrategy.logIn(credentials);
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localPasswordHash);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
// Initialize credentials with only userKey
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
decUserKey, // Pass userKey
null, // No masterKey
null // No masterKeyHash
);
// Call logIn
await passwordlessLoginStrategy.logIn(credentials);
// setMasterKey and setMasterKeyHash should not be called
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled();
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
// trustDeviceIfRequired should be called
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
});
});

View File

@ -5,6 +5,7 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result";
@ -40,7 +41,8 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService
twoFactorService: TwoFactorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
) {
super(
cryptoService,
@ -80,13 +82,32 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
}
protected override async setMasterKey(response: IdentityTokenResponse) {
await this.cryptoService.setMasterKey(this.passwordlessCredentials.decKey);
await this.cryptoService.setMasterKeyHash(this.passwordlessCredentials.localPasswordHash);
if (
this.passwordlessCredentials.decryptedMasterKey &&
this.passwordlessCredentials.decryptedMasterKeyHash
) {
await this.cryptoService.setMasterKey(this.passwordlessCredentials.decryptedMasterKey);
await this.cryptoService.setMasterKeyHash(
this.passwordlessCredentials.decryptedMasterKeyHash
);
}
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
// User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (this.passwordlessCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(this.passwordlessCredentials.decryptedUserKey);
} else {
await this.trySetUserKeyWithMasterKey();
// Establish trust if required after setting user key
await this.deviceTrustCryptoService.trustDeviceIfRequired();
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);

View File

@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
@ -15,6 +16,7 @@ import {
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
@ -38,6 +40,8 @@ describe("SsoLogInStrategy", () => {
let twoFactorService: MockProxy<TwoFactorService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestCryptoService: MockProxy<AuthRequestCryptoServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let ssoLogInStrategy: SsoLogInStrategy;
let credentials: SsoLogInCredentials;
@ -62,6 +66,8 @@ describe("SsoLogInStrategy", () => {
twoFactorService = mock<TwoFactorService>();
keyConnectorService = mock<KeyConnectorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestCryptoService = mock<AuthRequestCryptoServiceAbstraction>();
i18nService = mock<I18nService>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@ -78,7 +84,9 @@ describe("SsoLogInStrategy", () => {
stateService,
twoFactorService,
keyConnectorService,
deviceTrustCryptoService
deviceTrustCryptoService,
authRequestCryptoService,
i18nService
);
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
});

View File

@ -1,10 +1,12 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
@ -36,7 +38,9 @@ export class SsoLogInStrategy extends LogInStrategy {
stateService: StateService,
twoFactorService: TwoFactorService,
private keyConnectorService: KeyConnectorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authReqCryptoService: AuthRequestCryptoServiceAbstraction,
private i18nService: I18nService
) {
super(
cryptoService,
@ -70,6 +74,8 @@ export class SsoLogInStrategy extends LogInStrategy {
}
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
// TODO: discuss how this is no longer true with TDE
// eventually well need to support migration of existing TDE users to Key Connector
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
@ -81,33 +87,85 @@ export class SsoLogInStrategy extends LogInStrategy {
}
}
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
// If new user, return b/c we can't set the user key yet
if (tokenResponse.key === null) {
return;
const masterKeyEncryptedUserKey = tokenResponse.key;
// Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users
// on account creation and subsequent logins (confirmed or unconfirmed)
// but that is fine for TDE so we cannot return if it is undefined
if (masterKeyEncryptedUserKey) {
// set the master key encrypted user key if it exists
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
}
// Existing user; proceed
// User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless
await this.cryptoService.setMasterKeyEncryptedUserKey(tokenResponse.key);
// TODO: also admin approval request existence check should go here b/c that can give us a decrypted user key to set
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
// Note: TDE and key connector are mutually exclusive
if (userDecryptionOptions?.trustedDeviceOption) {
await this.trySetUserKeyWithDeviceKey(tokenResponse);
await this.trySetUserKeyWithApprovedAdminRequestIfExists();
const hasUserKey = await this.cryptoService.hasUserKey();
// Only try to set user key with device key if admin approval request was not successful
if (!hasUserKey) {
await this.trySetUserKeyWithDeviceKey(tokenResponse);
}
} else if (
// TODO: remove tokenResponse.keyConnectorUrl when it's deprecated
tokenResponse.keyConnectorUrl ||
userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl
masterKeyEncryptedUserKey != null &&
(tokenResponse.keyConnectorUrl || userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl)
) {
// Key connector enabled for user
await this.trySetUserKeyWithMasterKey();
}
// Note: In the traditional SSO flow with MP without key connector, the lock component
// is responsible for deriving master key from MP entry and then decrypting the user key
}
private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise<void> {
// At this point a user could have an admin auth request that has been approved
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
if (!adminAuthReqStorable) {
return;
}
// Call server to see if admin auth request has been approved
const adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
if (adminAuthReqResponse?.requestApproved) {
// if masterPasswordHash has a value, we will always receive authReqResponse.key
// as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
if (adminAuthReqResponse.masterPasswordHash) {
await this.authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
} else {
// if masterPasswordHash is null, we will always receive authReqResponse.key
// as authRequestPublicKey(userKey)
await this.authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
}
if (await this.cryptoService.hasUserKey()) {
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustCryptoService.trustDeviceIfRequired();
// if we successfully decrypted the user key, we can delete the admin auth request out of state
// TODO: eventually we post and clean up DB as well once consumed on client
await this.stateService.setAdminAuthRequest(null);
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
}
}
}
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {

View File

@ -0,0 +1,41 @@
import { Utils } from "../../../platform/misc/utils";
// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future
// type StorageShape {
// id: string;
// privateKey: string;
// }
// so we can get rid of the any type passed into fromJSON and coming out of ToJSON
export class AdminAuthRequestStorable {
id: string;
privateKey: ArrayBuffer;
constructor(init?: Partial<AdminAuthRequestStorable>) {
if (init) {
Object.assign(this, init);
}
}
toJSON() {
return {
id: this.id,
privateKey: Utils.fromBufferToByteString(this.privateKey),
};
}
static fromJSON(obj: any): AdminAuthRequestStorable {
if (obj == null) {
return null;
}
let privateKeyBuffer = null;
if (obj.privateKey) {
privateKeyBuffer = Utils.fromByteStringToArray(obj.privateKey)?.buffer;
}
return new AdminAuthRequestStorable({
id: obj.id,
privateKey: privateKeyBuffer,
});
}
}

View File

@ -1,4 +1,4 @@
import { MasterKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { MasterKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { AuthenticationType } from "../../enums/authentication-type";
import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request";
@ -38,8 +38,9 @@ export class PasswordlessLogInCredentials {
public email: string,
public accessCode: string,
public authRequestId: string,
public decKey: MasterKey,
public localPasswordHash: string,
public decryptedUserKey: UserKey,
public decryptedMasterKey: MasterKey,
public decryptedMasterKeyHash: string,
public twoFactor?: TokenTwoFactorRequest
) {}
}

View File

@ -0,0 +1,81 @@
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { Utils } from "../../platform/misc/utils";
import {
UserKey,
SymmetricCryptoKey,
MasterKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestResponse } from "../models/response/auth-request.response";
export class AuthRequestCryptoServiceImplementation implements AuthRequestCryptoServiceAbstraction {
constructor(private cryptoService: CryptoService) {}
async setUserKeyAfterDecryptingSharedUserKey(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) {
const userKey = await this.decryptPubKeyEncryptedUserKey(
authReqResponse.key,
authReqPrivateKey
);
await this.cryptoService.setUserKey(userKey);
}
async setKeysAfterDecryptingSharedMasterKeyAndHash(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) {
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
authReqResponse.key,
authReqResponse.masterPasswordHash,
authReqPrivateKey
);
// Decrypt and set user key in state
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setMasterKeyHash(masterKeyHash);
await this.cryptoService.setUserKey(userKey);
}
// Decryption helpers
async decryptPubKeyEncryptedUserKey(
pubKeyEncryptedUserKey: string,
privateKey: ArrayBuffer
): Promise<UserKey> {
const decryptedUserKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedUserKey,
privateKey
);
return new SymmetricCryptoKey(decryptedUserKeyArrayBuffer) as UserKey;
}
async decryptPubKeyEncryptedMasterKeyAndHash(
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: ArrayBuffer
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKey,
privateKey
);
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKeyHash,
privateKey
);
const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey;
const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer);
return {
masterKey,
masterKeyHash,
};
}
}

View File

@ -0,0 +1,169 @@
import { mock } from "jest-mock-extended";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestResponse } from "../models/response/auth-request.response";
import { AuthRequestCryptoServiceImplementation } from "./auth-request-crypto.service.implementation";
describe("AuthRequestCryptoService", () => {
let authReqCryptoService: AuthRequestCryptoServiceAbstraction;
const cryptoService = mock<CryptoService>();
let mockPrivateKey: ArrayBuffer;
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
authReqCryptoService = new AuthRequestCryptoServiceImplementation(cryptoService);
mockPrivateKey = new ArrayBuffer(64);
});
it("instantiates", () => {
expect(authReqCryptoService).not.toBeFalsy();
});
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
it("decrypts and sets user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedUserKey",
} as AuthRequestResponse;
const mockDecryptedUserKey = {} as UserKey;
jest
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedUserKey")
.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
mockAuthReqResponse,
mockPrivateKey
);
// Assert
expect(authReqCryptoService.decryptPubKeyEncryptedUserKey).toBeCalledWith(
mockAuthReqResponse.key,
mockPrivateKey
);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => {
it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedMasterKey",
masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash",
} as AuthRequestResponse;
const mockDecryptedMasterKey = {} as MasterKey;
const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash";
const mockDecryptedUserKey = {} as UserKey;
jest
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedMasterKeyAndHash")
.mockResolvedValueOnce({
masterKey: mockDecryptedMasterKey,
masterKeyHash: mockDecryptedMasterKeyHash,
});
cryptoService.setMasterKey.mockResolvedValueOnce(undefined);
cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
mockAuthReqResponse,
mockPrivateKey
);
// Assert
expect(authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith(
mockAuthReqResponse.key,
mockAuthReqResponse.masterPasswordHash,
mockPrivateKey
);
expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash);
expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedUserKey", () => {
it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedUserKey = "pubKeyEncryptedUserKey";
const mockDecryptedUserKeyArrayBuffer = new ArrayBuffer(64);
const mockDecryptedUserKey = new SymmetricCryptoKey(
mockDecryptedUserKeyArrayBuffer
) as UserKey;
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyArrayBuffer);
// Act
const result = await authReqCryptoService.decryptPubKeyEncryptedUserKey(
mockPubKeyEncryptedUserKey,
mockPrivateKey
);
// Assert
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
expect(result).toEqual(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedMasterKeyAndHash", () => {
it("returns a decrypted master key and hash when given a valid public key encrypted master key, public key encrypted master key hash, and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedMasterKey = "pubKeyEncryptedMasterKey";
const mockPubKeyEncryptedMasterKeyHash = "pubKeyEncryptedMasterKeyHash";
const mockDecryptedMasterKeyArrayBuffer = new ArrayBuffer(64);
const mockDecryptedMasterKey = new SymmetricCryptoKey(
mockDecryptedMasterKeyArrayBuffer
) as MasterKey;
const mockDecryptedMasterKeyHashArrayBuffer = new ArrayBuffer(64);
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(
mockDecryptedMasterKeyHashArrayBuffer
);
cryptoService.rsaDecrypt
.mockResolvedValueOnce(mockDecryptedMasterKeyArrayBuffer)
.mockResolvedValueOnce(mockDecryptedMasterKeyHashArrayBuffer);
// Act
const result = await authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash(
mockPubKeyEncryptedMasterKey,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey
);
// Assert
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
1,
mockPubKeyEncryptedMasterKey,
mockPrivateKey
);
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
2,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey
);
expect(result.masterKey).toEqual(mockDecryptedMasterKey);
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
});
});
});

View File

@ -18,6 +18,7 @@ import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
@ -105,7 +106,8 @@ export class AuthService implements AuthServiceAbstraction {
protected encryptService: EncryptService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {}
async logIn(
@ -152,7 +154,9 @@ export class AuthService implements AuthServiceAbstraction {
this.stateService,
this.twoFactorService,
this.keyConnectorService,
this.deviceTrustCryptoService
this.deviceTrustCryptoService,
this.authReqCryptoService,
this.i18nService
);
break;
case AuthenticationType.UserApi:
@ -180,7 +184,8 @@ export class AuthService implements AuthServiceAbstraction {
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService
this.twoFactorService,
this.deviceTrustCryptoService
);
break;
}
@ -299,24 +304,34 @@ export class AuthService implements AuthServiceAbstraction {
key: string,
requestApproved: boolean
): Promise<AuthRequestResponse> {
// TODO: This currently depends on always having the Master Key and MP Hash
// We need to change this to using a different method (possibly server auth code + user key)
const pubKey = Utils.fromB64ToArray(key);
const masterKey = await this.cryptoService.getMasterKey();
if (!masterKey) {
throw new Error("Master key not found");
}
const encryptedKey = await this.cryptoService.rsaEncrypt(masterKey.encKey, pubKey.buffer);
let encryptedMasterPasswordHash = null;
if ((await this.stateService.getKeyHash()) != null) {
encryptedMasterPasswordHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
pubKey.buffer
);
let keyToEncrypt;
let encryptedMasterKeyHash = null;
if (masterKey) {
keyToEncrypt = masterKey.encKey;
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
const masterKeyHash = await this.stateService.getKeyHash();
if (masterKeyHash != null) {
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey.buffer
);
}
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey.buffer);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterPasswordHash.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
requestApproved
);

View File

@ -3,6 +3,8 @@ import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import {
@ -21,12 +23,14 @@ import {
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected stateService: StateService,
protected appIdService: AppIdService,
protected devicesApiService: DevicesApiServiceAbstraction
private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private stateService: StateService,
private appIdService: AppIdService,
private devicesApiService: DevicesApiServiceAbstraction,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {}
/**
@ -41,6 +45,15 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
await this.stateService.setShouldTrustDevice(value);
}
async trustDeviceIfRequired(): Promise<void> {
const shouldTrustDevice = await this.getShouldTrustDevice();
if (shouldTrustDevice) {
await this.trustDevice();
// reset the trust choice
await this.setShouldTrustDevice(false);
}
}
async trustDevice(): Promise<DeviceResponse> {
// Attempt to get user key
const userKey: UserKey = await this.cryptoService.getUserKey();
@ -84,6 +97,9 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac
// store device key in local/secure storage if enc keys posted to server successfully
await this.setDeviceKey(deviceKey);
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
return deviceResponse;
}

View File

@ -1,11 +1,14 @@
import { matches, mock, mockReset } from "jest-mock-extended";
import { matches, mock } from "jest-mock-extended";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { DeviceType } from "../../enums";
import { EncryptionType } from "../../enums/encryption-type.enum";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import {
@ -13,14 +16,12 @@ import {
DeviceKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "../../platform/services/crypto.service";
import { CsprngArray } from "../../types/csprng";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
describe("deviceTrustCryptoService", () => {
let deviceTrustCryptoService: DeviceTrustCryptoService;
@ -30,13 +31,11 @@ describe("deviceTrustCryptoService", () => {
const stateService = mock<StateService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
const i18nService = mock<I18nService>();
const platformUtilsService = mock<PlatformUtilsService>();
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
mockReset(stateService);
mockReset(appIdService);
mockReset(devicesApiService);
jest.clearAllMocks();
deviceTrustCryptoService = new DeviceTrustCryptoService(
cryptoFunctionService,
@ -44,7 +43,9 @@ describe("deviceTrustCryptoService", () => {
encryptService,
stateService,
appIdService,
devicesApiService
devicesApiService,
i18nService,
platformUtilsService
);
});
@ -79,6 +80,34 @@ describe("deviceTrustCryptoService", () => {
});
});
describe("trustDeviceIfRequired", () => {
it("should trust device and reset when getShouldTrustDevice returns true", async () => {
jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true);
jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse);
jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue();
await deviceTrustCryptoService.trustDeviceIfRequired();
expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(false);
});
it("should not trust device nor reset when getShouldTrustDevice returns false", async () => {
const getShouldTrustDeviceSpy = jest
.spyOn(deviceTrustCryptoService, "getShouldTrustDevice")
.mockResolvedValue(false);
const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice");
const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice");
await deviceTrustCryptoService.trustDeviceIfRequired();
expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(trustDeviceSpy).not.toHaveBeenCalled();
expect(setShouldTrustDeviceSpy).not.toHaveBeenCalled();
});
});
describe("Trusted Device Encryption core logic tests", () => {
const deviceKeyBytesLength = 64;
const userKeyBytesLength = 64;

View File

@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
@ -31,6 +32,38 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
);
});
describe("enrollIfRequired", () => {
it("should not enroll when user is already enrolled in password reset", async () => {
const mockResponse = new OrganizationAutoEnrollStatusResponse({
ResetPasswordEnabled: true,
Id: "orgId",
});
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
const enrollSpy = jest.spyOn(service, "enroll");
enrollSpy.mockResolvedValue();
await service.enrollIfRequired("ssoId");
expect(service.enroll).not.toHaveBeenCalled();
});
it("should enroll when user is not enrolled in password reset", async () => {
const mockResponse = new OrganizationAutoEnrollStatusResponse({
ResetPasswordEnabled: false,
Id: "orgId",
});
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
const enrollSpy = jest.spyOn(service, "enroll");
enrollSpy.mockResolvedValue();
await service.enrollIfRequired("ssoId");
expect(service.enroll).toHaveBeenCalled();
});
});
describe("enroll", () => {
it("should throw an error if the organization keys are not found", async () => {
organizationApiService.getKeys.mockResolvedValue(null);

View File

@ -19,6 +19,16 @@ export class PasswordResetEnrollmentServiceImplementation
protected i18nService: I18nService
) {}
async enrollIfRequired(organizationSsoIdentifier: string): Promise<void> {
const orgAutoEnrollStatusResponse = await this.organizationApiService.getAutoEnrollStatus(
organizationSsoIdentifier
);
if (!orgAutoEnrollStatusResponse.resetPasswordEnabled) {
await this.enroll(orgAutoEnrollStatusResponse.id, null, null);
}
}
async enroll(organizationId: string): Promise<void>;
async enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise<void> {

View File

@ -5,6 +5,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@ -262,6 +263,11 @@ export abstract class StateService<T extends Account = Account> {
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
setAdminAuthRequest: (
adminAuthRequest: AdminAuthRequestStorable,
options?: StorageOptions
) => Promise<void>;
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
getAccountDecryptionOptions: (

View File

@ -6,6 +6,7 @@ import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { Policy } from "../../../admin-console/models/domain/policy";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
@ -369,6 +370,7 @@ export class Account {
settings?: AccountSettings = new AccountSettings();
tokens?: AccountTokens = new AccountTokens();
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
adminAuthRequest?: AdminAuthRequestStorable = null;
constructor(init: Partial<Account>) {
Object.assign(this, {
@ -396,6 +398,9 @@ export class Account {
...new AccountDecryptionOptions(),
...init?.decryptionOptions,
},
adminAuthRequest: init?.adminAuthRequest
? new AdminAuthRequestStorable(init?.adminAuthRequest)
: null,
});
}
@ -410,6 +415,7 @@ export class Account {
settings: AccountSettings.fromJSON(json?.settings),
tokens: AccountTokens.fromJSON(json?.tokens),
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
});
}
}

View File

@ -6,6 +6,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@ -1326,6 +1327,37 @@ export class StateService<
await this.saveAccount(account, options);
}
async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.adminAuthRequest
? AdminAuthRequestStorable.fromJSON(account.adminAuthRequest)
: null;
}
async setAdminAuthRequest(
adminAuthRequest: AdminAuthRequestStorable,
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.adminAuthRequest = adminAuthRequest;
await this.saveAccount(account, options);
}
async getShouldTrustDevice(options?: StorageOptions): Promise<boolean> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
@ -3112,11 +3144,12 @@ export class StateService<
}
}
// settings persist even on reset, and are not effected by this method
// settings persist even on reset, and are not affected by this method
protected resetAccount(account: TAccount) {
const persistentAccountInformation = {
settings: account.settings,
keys: { deviceKey: account.keys.deviceKey },
adminAuthRequest: account.adminAuthRequest,
};
return Object.assign(this.createAccount(), persistentAccountInformation);
}

View File

@ -254,6 +254,10 @@ export class ApiService implements ApiServiceAbstraction {
const r = await this.send("POST", "/auth-requests/", request, false, true);
return new AuthRequestResponse(r);
}
async postAdminAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
return new AuthRequestResponse(r);
}
async getAuthResponse(id: string, accessCode: string): Promise<AuthRequestResponse> {
const path = `/auth-requests/${id}/response?code=${accessCode}`;