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:
parent
79d186f4f5
commit
0b861f4d0c
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
);
|
||||
}
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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 () =>
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,8 +232,6 @@ export class SsoComponent {
|
||||
return await this.handleSuccessfulLogin();
|
||||
} catch (e) {
|
||||
await this.handleLoginError(e);
|
||||
} finally {
|
||||
this.loggingIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -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>;
|
||||
|
@ -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 }>;
|
||||
}
|
@ -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: (
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,5 @@
|
||||
export enum AuthRequestType {
|
||||
AuthenticateAndUnlock = 0,
|
||||
Unlock = 1,
|
||||
AdminApproval = 2,
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 we’ll 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> {
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
) {}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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> {
|
||||
|
@ -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: (
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}`;
|
||||
|
Loading…
Reference in New Issue
Block a user