[PM-5499] auth request service migrations (#8597)
* move auth request storage to service * create migrations for auth requests * fix tests * fix browser * fix login strategy * update migration * use correct test descriptions in migration
This commit is contained in:
parent
d0bcc75721
commit
576431d29e
|
@ -17,6 +17,10 @@ import {
|
||||||
FactoryOptions,
|
FactoryOptions,
|
||||||
factory,
|
factory,
|
||||||
} from "../../../platform/background/service-factories/factory-options";
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import {
|
||||||
|
stateProviderFactory,
|
||||||
|
StateProviderInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
|
||||||
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
|
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
|
||||||
import {
|
import {
|
||||||
|
@ -31,7 +35,8 @@ export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions &
|
||||||
AccountServiceInitOptions &
|
AccountServiceInitOptions &
|
||||||
MasterPasswordServiceInitOptions &
|
MasterPasswordServiceInitOptions &
|
||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
ApiServiceInitOptions;
|
ApiServiceInitOptions &
|
||||||
|
StateProviderInitOptions;
|
||||||
|
|
||||||
export function authRequestServiceFactory(
|
export function authRequestServiceFactory(
|
||||||
cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices,
|
cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices,
|
||||||
|
@ -48,6 +53,7 @@ export function authRequestServiceFactory(
|
||||||
await internalMasterPasswordServiceFactory(cache, opts),
|
await internalMasterPasswordServiceFactory(cache, opts),
|
||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
await apiServiceFactory(cache, opts),
|
await apiServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -596,6 +596,7 @@ export default class MainBackground {
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.authService = new AuthService(
|
this.authService = new AuthService(
|
||||||
|
@ -844,6 +845,7 @@ export default class MainBackground {
|
||||||
logoutCallback,
|
logoutCallback,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.authService,
|
this.authService,
|
||||||
|
this.authRequestService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -483,6 +483,7 @@ export class Main {
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { FormBuilder } from "@angular/forms";
|
||||||
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
|
||||||
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
@ -20,6 +21,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio
|
||||||
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
||||||
|
@ -60,6 +62,7 @@ export class SettingsComponent implements OnInit {
|
||||||
showAppPreferences = true;
|
showAppPreferences = true;
|
||||||
|
|
||||||
currentUserEmail: string;
|
currentUserEmail: string;
|
||||||
|
currentUserId: UserId;
|
||||||
|
|
||||||
availableVaultTimeoutActions$: Observable<VaultTimeoutAction[]>;
|
availableVaultTimeoutActions$: Observable<VaultTimeoutAction[]>;
|
||||||
vaultTimeoutPolicyCallout: Observable<{
|
vaultTimeoutPolicyCallout: Observable<{
|
||||||
|
@ -122,6 +125,7 @@ export class SettingsComponent implements OnInit {
|
||||||
private desktopSettingsService: DesktopSettingsService,
|
private desktopSettingsService: DesktopSettingsService,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
|
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
|
||||||
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
||||||
|
|
||||||
|
@ -207,6 +211,7 @@ export class SettingsComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.currentUserEmail = await this.stateService.getEmail();
|
this.currentUserEmail = await this.stateService.getEmail();
|
||||||
|
this.currentUserId = (await this.stateService.getUserId()) as UserId;
|
||||||
|
|
||||||
this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe(
|
this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe(
|
||||||
switchMap(() => this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()),
|
switchMap(() => this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()),
|
||||||
|
@ -249,7 +254,8 @@ export class SettingsComponent implements OnInit {
|
||||||
requirePasswordOnStart: await firstValueFrom(
|
requirePasswordOnStart: await firstValueFrom(
|
||||||
this.biometricStateService.requirePasswordOnStart$,
|
this.biometricStateService.requirePasswordOnStart$,
|
||||||
),
|
),
|
||||||
approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false,
|
approveLoginRequests:
|
||||||
|
(await this.authRequestService.getAcceptAuthRequests(this.currentUserId)) ?? false,
|
||||||
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
|
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
|
||||||
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
|
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
|
||||||
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
|
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
|
||||||
|
@ -665,7 +671,10 @@ export class SettingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateApproveLoginRequests() {
|
async updateApproveLoginRequests() {
|
||||||
await this.stateService.setApproveLoginRequests(this.form.value.approveLoginRequests);
|
await this.authRequestService.setAcceptAuthRequests(
|
||||||
|
this.form.value.approveLoginRequests,
|
||||||
|
this.currentUserId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
|
@ -16,13 +16,13 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
@ -32,6 +32,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { AuthRequestServiceAbstraction } from "../../../../../../libs/auth/src/common/abstractions";
|
||||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||||
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
||||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||||
|
@ -102,11 +103,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private stateService: StateService,
|
|
||||||
private searchBarService: SearchBarService,
|
private searchBarService: SearchBarService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -224,7 +226,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
this.searchBarService.setEnabled(true);
|
this.searchBarService.setEnabled(true);
|
||||||
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
||||||
|
|
||||||
const approveLoginRequests = await this.stateService.getApproveLoginRequests();
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
|
const approveLoginRequests = await this.authRequestService.getAcceptAuthRequests(userId);
|
||||||
if (approveLoginRequests) {
|
if (approveLoginRequests) {
|
||||||
const authRequest = await this.apiService.getLastAuthRequest();
|
const authRequest = await this.apiService.getLastAuthRequest();
|
||||||
if (authRequest != null) {
|
if (authRequest != null) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||||
|
|
||||||
|
@ -131,6 +132,7 @@ export class LoginViaAuthRequestComponent
|
||||||
// This also prevents it from being lost on refresh as the
|
// This also prevents it from being lost on refresh as the
|
||||||
// login service email does not persist.
|
// login service email does not persist.
|
||||||
this.email = await this.stateService.getEmail();
|
this.email = await this.stateService.getEmail();
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
|
|
||||||
if (!this.email) {
|
if (!this.email) {
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
|
||||||
|
@ -142,10 +144,10 @@ export class LoginViaAuthRequestComponent
|
||||||
|
|
||||||
// We only allow a single admin approval request to be active at a time
|
// 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
|
// so must check state to see if we have an existing one or not
|
||||||
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
|
const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId);
|
||||||
|
|
||||||
if (adminAuthReqStorable) {
|
if (adminAuthReqStorable) {
|
||||||
await this.handleExistingAdminAuthRequest(adminAuthReqStorable);
|
await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId);
|
||||||
} else {
|
} else {
|
||||||
// No existing admin auth request; so we need to create one
|
// No existing admin auth request; so we need to create one
|
||||||
await this.startAuthRequestLogin();
|
await this.startAuthRequestLogin();
|
||||||
|
@ -173,7 +175,10 @@ export class LoginViaAuthRequestComponent
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) {
|
private async handleExistingAdminAuthRequest(
|
||||||
|
adminAuthReqStorable: AdminAuthRequestStorable,
|
||||||
|
userId: UserId,
|
||||||
|
) {
|
||||||
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
|
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
|
||||||
// has been approved and handle it if so.
|
// has been approved and handle it if so.
|
||||||
|
|
||||||
|
@ -183,13 +188,13 @@ export class LoginViaAuthRequestComponent
|
||||||
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
|
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||||
return await this.handleExistingAdminAuthReqDeletedOrDenied();
|
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request doesn't exist anymore
|
// Request doesn't exist anymore
|
||||||
if (!adminAuthReqResponse) {
|
if (!adminAuthReqResponse) {
|
||||||
return await this.handleExistingAdminAuthReqDeletedOrDenied();
|
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-derive the user's fingerprint phrase
|
// Re-derive the user's fingerprint phrase
|
||||||
|
@ -203,7 +208,7 @@ export class LoginViaAuthRequestComponent
|
||||||
|
|
||||||
// Request denied
|
// Request denied
|
||||||
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
|
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
|
||||||
return await this.handleExistingAdminAuthReqDeletedOrDenied();
|
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request approved
|
// Request approved
|
||||||
|
@ -211,6 +216,7 @@ export class LoginViaAuthRequestComponent
|
||||||
return await this.handleApprovedAdminAuthRequest(
|
return await this.handleApprovedAdminAuthRequest(
|
||||||
adminAuthReqResponse,
|
adminAuthReqResponse,
|
||||||
adminAuthReqStorable.privateKey,
|
adminAuthReqStorable.privateKey,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,9 +225,9 @@ export class LoginViaAuthRequestComponent
|
||||||
await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
|
await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleExistingAdminAuthReqDeletedOrDenied() {
|
private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) {
|
||||||
// clear the admin auth request from state
|
// clear the admin auth request from state
|
||||||
await this.stateService.setAdminAuthRequest(null);
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||||
|
|
||||||
// start new auth request
|
// start new auth request
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
@ -269,7 +275,8 @@ export class LoginViaAuthRequestComponent
|
||||||
privateKey: this.authRequestKeyPair.privateKey,
|
privateKey: this.authRequestKeyPair.privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.stateService.setAdminAuthRequest(adminAuthReqStorable);
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
|
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
|
||||||
} else {
|
} else {
|
||||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||||
reqResponse = await this.apiService.postAuthRequest(this.authRequest);
|
reqResponse = await this.apiService.postAuthRequest(this.authRequest);
|
||||||
|
@ -333,9 +340,11 @@ export class LoginViaAuthRequestComponent
|
||||||
|
|
||||||
// if user has authenticated via SSO
|
// if user has authenticated via SSO
|
||||||
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
|
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
return await this.handleApprovedAdminAuthRequest(
|
return await this.handleApprovedAdminAuthRequest(
|
||||||
authReqResponse,
|
authReqResponse,
|
||||||
this.authRequestKeyPair.privateKey,
|
this.authRequestKeyPair.privateKey,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +372,7 @@ export class LoginViaAuthRequestComponent
|
||||||
async handleApprovedAdminAuthRequest(
|
async handleApprovedAdminAuthRequest(
|
||||||
adminAuthReqResponse: AuthRequestResponse,
|
adminAuthReqResponse: AuthRequestResponse,
|
||||||
privateKey: ArrayBuffer,
|
privateKey: ArrayBuffer,
|
||||||
|
userId: UserId,
|
||||||
) {
|
) {
|
||||||
// See verifyAndHandleApprovedAuthReq(...) for flow details
|
// See verifyAndHandleApprovedAuthReq(...) for flow details
|
||||||
// it's flow 2 or 3 based on presence of masterPasswordHash
|
// it's flow 2 or 3 based on presence of masterPasswordHash
|
||||||
|
@ -384,7 +394,7 @@ export class LoginViaAuthRequestComponent
|
||||||
|
|
||||||
// clear the admin auth request from state so it cannot be used again (it's a one time use)
|
// 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
|
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
||||||
await this.stateService.setAdminAuthRequest(null);
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
||||||
|
|
||||||
|
|
|
@ -740,6 +740,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
|
AuthRequestServiceAbstraction,
|
||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
@ -963,6 +964,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|
|
@ -1,12 +1,52 @@
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
export abstract class AuthRequestServiceAbstraction {
|
export abstract class AuthRequestServiceAbstraction {
|
||||||
/** Emits an auth request id when an auth request has been approved. */
|
/** Emits an auth request id when an auth request has been approved. */
|
||||||
authRequestPushNotification$: Observable<string>;
|
authRequestPushNotification$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user has chosen to allow auth requests to show on this client.
|
||||||
|
* Intended to prevent spamming the user with auth requests.
|
||||||
|
* @param userId The user id.
|
||||||
|
* @throws If `userId` is not provided.
|
||||||
|
*/
|
||||||
|
abstract getAcceptAuthRequests: (userId: UserId) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Sets whether to allow auth requests to show on this client for this user.
|
||||||
|
* @param accept Whether to allow auth requests to show on this client.
|
||||||
|
* @param userId The user id.
|
||||||
|
* @throws If `userId` is not provided.
|
||||||
|
*/
|
||||||
|
abstract setAcceptAuthRequests: (accept: boolean, userId: UserId) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Returns an admin auth request for the given user if it exists.
|
||||||
|
* @param userId The user id.
|
||||||
|
* @throws If `userId` is not provided.
|
||||||
|
*/
|
||||||
|
abstract getAdminAuthRequest: (userId: UserId) => Promise<AdminAuthRequestStorable | null>;
|
||||||
|
/**
|
||||||
|
* Sets an admin auth request for the given user.
|
||||||
|
* Note: use {@link clearAdminAuthRequest} to clear the request.
|
||||||
|
* @param authRequest The admin auth request.
|
||||||
|
* @param userId The user id.
|
||||||
|
* @throws If `authRequest` or `userId` is not provided.
|
||||||
|
*/
|
||||||
|
abstract setAdminAuthRequest: (
|
||||||
|
authRequest: AdminAuthRequestStorable,
|
||||||
|
userId: UserId,
|
||||||
|
) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Clears an admin auth request for the given user.
|
||||||
|
* @param userId The user id.
|
||||||
|
* @throws If `userId` is not provided.
|
||||||
|
*/
|
||||||
|
abstract clearAdminAuthRequest: (userId: UserId) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Approve or deny an auth request.
|
* Approve or deny an auth request.
|
||||||
* @param approve True to approve, false to deny.
|
* @param approve True to approve, false to deny.
|
||||||
|
|
|
@ -132,7 +132,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
protected override async setUserKey(
|
||||||
|
response: IdentityTokenResponse,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||||
// User now may or may not have a master password
|
// User now may or may not have a master password
|
||||||
// but set the master key encrypted user key if it exists regardless
|
// but set the master key encrypted user key if it exists regardless
|
||||||
|
@ -143,7 +146,6 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||||
} else {
|
} else {
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey();
|
||||||
|
|
||||||
const userId = (await this.stateService.getUserId()) as UserId;
|
|
||||||
// Establish trust if required after setting user key
|
// Establish trust if required after setting user key
|
||||||
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
|
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
AccountProfile,
|
AccountProfile,
|
||||||
AccountTokens,
|
AccountTokens,
|
||||||
} from "@bitwarden/common/platform/models/domain/account";
|
} from "@bitwarden/common/platform/models/domain/account";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||||
import {
|
import {
|
||||||
|
@ -160,14 +161,11 @@ export abstract class LoginStrategy {
|
||||||
* @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token.
|
* @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token.
|
||||||
* @returns {Promise<void>} - A promise that resolves when the account information has been successfully saved.
|
* @returns {Promise<void>} - A promise that resolves when the account information has been successfully saved.
|
||||||
*/
|
*/
|
||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<void> {
|
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
||||||
const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken);
|
const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken);
|
||||||
|
|
||||||
const userId = accountInformation.sub;
|
const userId = accountInformation.sub;
|
||||||
|
|
||||||
// If you don't persist existing admin auth requests on login, they will get deleted.
|
|
||||||
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
|
|
||||||
|
|
||||||
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
|
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
|
||||||
const vaultTimeout = await this.stateService.getVaultTimeout();
|
const vaultTimeout = await this.stateService.getVaultTimeout();
|
||||||
|
|
||||||
|
@ -197,7 +195,6 @@ export abstract class LoginStrategy {
|
||||||
tokens: {
|
tokens: {
|
||||||
...new AccountTokens(),
|
...new AccountTokens(),
|
||||||
},
|
},
|
||||||
adminAuthRequest: adminAuthRequest?.toJSON(),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -206,6 +203,7 @@ export abstract class LoginStrategy {
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
|
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
|
||||||
|
return userId as UserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
||||||
|
@ -228,7 +226,7 @@ export abstract class LoginStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must come before setting keys, user key needs email to update additional keys
|
// Must come before setting keys, user key needs email to update additional keys
|
||||||
await this.saveAccountInformation(response);
|
const userId = await this.saveAccountInformation(response);
|
||||||
|
|
||||||
if (response.twoFactorToken != null) {
|
if (response.twoFactorToken != null) {
|
||||||
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
||||||
|
@ -238,7 +236,7 @@ export abstract class LoginStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setMasterKey(response);
|
await this.setMasterKey(response);
|
||||||
await this.setUserKey(response);
|
await this.setUserKey(response, userId);
|
||||||
await this.setPrivateKey(response);
|
await this.setPrivateKey(response);
|
||||||
|
|
||||||
this.messagingService.send("loggedIn");
|
this.messagingService.send("loggedIn");
|
||||||
|
@ -248,7 +246,7 @@ export abstract class LoginStrategy {
|
||||||
|
|
||||||
// The keys comes from different sources depending on the login strategy
|
// The keys comes from different sources depending on the login strategy
|
||||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
|
|
||||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||||
|
@ -207,14 +208,16 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
protected override async setUserKey(
|
||||||
|
response: IdentityTokenResponse,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
// If migration is required, we won't have a user key to set yet.
|
// If migration is required, we won't have a user key to set yet.
|
||||||
if (this.encryptionKeyMigrationRequired(response)) {
|
if (this.encryptionKeyMigrationRequired(response)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||||
|
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
|
|
@ -301,7 +301,7 @@ describe("SsoLoginStrategy", () => {
|
||||||
id: "1",
|
id: "1",
|
||||||
privateKey: "PRIVATE" as any,
|
privateKey: "PRIVATE" as any,
|
||||||
} as AdminAuthRequestStorable;
|
} as AdminAuthRequestStorable;
|
||||||
stateService.getAdminAuthRequest.mockResolvedValue(
|
authRequestService.getAdminAuthRequest.mockResolvedValue(
|
||||||
new AdminAuthRequestStorable(adminAuthRequest),
|
new AdminAuthRequestStorable(adminAuthRequest),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -364,7 +364,7 @@ describe("SsoLoginStrategy", () => {
|
||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null);
|
expect(authRequestService.clearAdminAuthRequest).toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash,
|
authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash,
|
||||||
).not.toHaveBeenCalled();
|
).not.toHaveBeenCalled();
|
||||||
|
|
|
@ -216,7 +216,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
|
// 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?)
|
// 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> {
|
protected override async setUserKey(
|
||||||
|
tokenResponse: IdentityTokenResponse,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
const masterKeyEncryptedUserKey = tokenResponse.key;
|
const masterKeyEncryptedUserKey = tokenResponse.key;
|
||||||
|
|
||||||
// Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users
|
// Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users
|
||||||
|
@ -232,7 +235,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
// Note: TDE and key connector are mutually exclusive
|
// Note: TDE and key connector are mutually exclusive
|
||||||
if (userDecryptionOptions?.trustedDeviceOption) {
|
if (userDecryptionOptions?.trustedDeviceOption) {
|
||||||
await this.trySetUserKeyWithApprovedAdminRequestIfExists();
|
await this.trySetUserKeyWithApprovedAdminRequestIfExists(userId);
|
||||||
|
|
||||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||||
|
|
||||||
|
@ -252,9 +255,9 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
// is responsible for deriving master key from MP entry and then decrypting the user key
|
// is responsible for deriving master key from MP entry and then decrypting the user key
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise<void> {
|
private async trySetUserKeyWithApprovedAdminRequestIfExists(userId: UserId): Promise<void> {
|
||||||
// At this point a user could have an admin auth request that has been approved
|
// At this point a user could have an admin auth request that has been approved
|
||||||
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
|
const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId);
|
||||||
|
|
||||||
if (!adminAuthReqStorable) {
|
if (!adminAuthReqStorable) {
|
||||||
return;
|
return;
|
||||||
|
@ -268,7 +271,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||||
// if we get a 404, it means the auth request has been deleted so clear it from storage
|
// if we get a 404, it means the auth request has been deleted so clear it from storage
|
||||||
await this.stateService.setAdminAuthRequest(null);
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always return on an error here as we don't want to block the user from logging in
|
// Always return on an error here as we don't want to block the user from logging in
|
||||||
|
@ -295,12 +298,11 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
if (await this.cryptoService.hasUserKey()) {
|
if (await this.cryptoService.hasUserKey()) {
|
||||||
// Now that we have a decrypted user key in memory, we can check if we
|
// Now that we have a decrypted user key in memory, we can check if we
|
||||||
// need to establish trust on the current device
|
// need to establish trust on the current device
|
||||||
const userId = (await this.stateService.getUserId()) as UserId;
|
|
||||||
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
|
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
|
||||||
|
|
||||||
// if we successfully decrypted the user key, we can delete the admin auth request out of state
|
// 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
|
// TODO: eventually we post and clean up DB as well once consumed on client
|
||||||
await this.stateService.setAdminAuthRequest(null);
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
@ -97,7 +98,10 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
protected override async setUserKey(
|
||||||
|
response: IdentityTokenResponse,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||||
|
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
|
@ -116,8 +120,8 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
||||||
await super.saveAccountInformation(tokenResponse);
|
const userId = await super.saveAccountInformation(tokenResponse);
|
||||||
|
|
||||||
const vaultTimeout = await this.stateService.getVaultTimeout();
|
const vaultTimeout = await this.stateService.getVaultTimeout();
|
||||||
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
|
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
|
||||||
|
@ -134,6 +138,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
vaultTimeoutAction as VaultTimeoutAction,
|
vaultTimeoutAction as VaultTimeoutAction,
|
||||||
vaultTimeout,
|
vaultTimeout,
|
||||||
);
|
);
|
||||||
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportCache(): CacheData {
|
exportCache(): CacheData {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
|
||||||
|
@ -98,7 +99,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setUserKey(idTokenResponse: IdentityTokenResponse) {
|
protected override async setUserKey(idTokenResponse: IdentityTokenResponse, userId: UserId) {
|
||||||
const masterKeyEncryptedUserKey = idTokenResponse.key;
|
const masterKeyEncryptedUserKey = idTokenResponse.key;
|
||||||
|
|
||||||
if (masterKeyEncryptedUserKey) {
|
if (masterKeyEncryptedUserKey) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
@ -18,6 +19,7 @@ import { AuthRequestService } from "./auth-request.service";
|
||||||
describe("AuthRequestService", () => {
|
describe("AuthRequestService", () => {
|
||||||
let sut: AuthRequestService;
|
let sut: AuthRequestService;
|
||||||
|
|
||||||
|
const stateProvider = mock<StateProvider>();
|
||||||
let accountService: FakeAccountService;
|
let accountService: FakeAccountService;
|
||||||
let masterPasswordService: FakeMasterPasswordService;
|
let masterPasswordService: FakeMasterPasswordService;
|
||||||
const appIdService = mock<AppIdService>();
|
const appIdService = mock<AppIdService>();
|
||||||
|
@ -38,6 +40,7 @@ describe("AuthRequestService", () => {
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
|
stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockPrivateKey = new Uint8Array(64);
|
mockPrivateKey = new Uint8Array(64);
|
||||||
|
@ -59,6 +62,31 @@ describe("AuthRequestService", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("AcceptAuthRequests", () => {
|
||||||
|
it("returns an error when userId isn't provided", async () => {
|
||||||
|
await expect(sut.getAcceptAuthRequests(undefined)).rejects.toThrow("User ID is required");
|
||||||
|
await expect(sut.setAcceptAuthRequests(true, undefined)).rejects.toThrow(
|
||||||
|
"User ID is required",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AdminAuthRequest", () => {
|
||||||
|
it("returns an error when userId isn't provided", async () => {
|
||||||
|
await expect(sut.getAdminAuthRequest(undefined)).rejects.toThrow("User ID is required");
|
||||||
|
await expect(sut.setAdminAuthRequest(undefined, undefined)).rejects.toThrow(
|
||||||
|
"User ID is required",
|
||||||
|
);
|
||||||
|
await expect(sut.clearAdminAuthRequest(undefined)).rejects.toThrow("User ID is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not allow clearing from setAdminAuthRequest", async () => {
|
||||||
|
await expect(sut.setAdminAuthRequest(null, "USER_ID" as UserId)).rejects.toThrow(
|
||||||
|
"Auth request is required",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("approveOrDenyAuthRequest", () => {
|
describe("approveOrDenyAuthRequest", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cryptoService.rsaEncrypt.mockResolvedValue({
|
cryptoService.rsaEncrypt.mockResolvedValue({
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { firstValueFrom, Observable, Subject } from "rxjs";
|
import { Observable, Subject, firstValueFrom } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||||
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||||
|
@ -10,10 +12,43 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import {
|
||||||
|
AUTH_REQUEST_DISK_LOCAL,
|
||||||
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
|
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disk-local to maintain consistency between tabs (even though
|
||||||
|
* approvals are currently only available on desktop). We don't
|
||||||
|
* want to clear this on logout as it's a user preference.
|
||||||
|
*/
|
||||||
|
export const ACCEPT_AUTH_REQUESTS_KEY = new UserKeyDefinition<boolean>(
|
||||||
|
AUTH_REQUEST_DISK_LOCAL,
|
||||||
|
"acceptAuthRequests",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value ?? false,
|
||||||
|
clearOn: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disk-local to maintain consistency between tabs. We don't want to
|
||||||
|
* clear this on logout since admin auth requests are long-lived.
|
||||||
|
*/
|
||||||
|
export const ADMIN_AUTH_REQUEST_KEY = new UserKeyDefinition<Jsonify<AdminAuthRequestStorable>>(
|
||||||
|
AUTH_REQUEST_DISK_LOCAL,
|
||||||
|
"adminAuthRequest",
|
||||||
|
{
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export class AuthRequestService implements AuthRequestServiceAbstraction {
|
export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||||
private authRequestPushNotificationSubject = new Subject<string>();
|
private authRequestPushNotificationSubject = new Subject<string>();
|
||||||
authRequestPushNotification$: Observable<string>;
|
authRequestPushNotification$: Observable<string>;
|
||||||
|
@ -24,10 +59,61 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
) {
|
) {
|
||||||
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
|
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAcceptAuthRequests(userId: UserId): Promise<boolean> {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("User ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = await firstValueFrom(
|
||||||
|
this.stateProvider.getUser(userId, ACCEPT_AUTH_REQUESTS_KEY).state$,
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAcceptAuthRequests(accept: boolean, userId: UserId): Promise<void> {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("User ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(ACCEPT_AUTH_REQUESTS_KEY, accept, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAdminAuthRequest(userId: UserId): Promise<AdminAuthRequestStorable | null> {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("User ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const authRequestSerialized = await firstValueFrom(
|
||||||
|
this.stateProvider.getUser(userId, ADMIN_AUTH_REQUEST_KEY).state$,
|
||||||
|
);
|
||||||
|
const adminAuthRequestStorable = AdminAuthRequestStorable.fromJSON(authRequestSerialized);
|
||||||
|
return adminAuthRequestStorable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAdminAuthRequest(authRequest: AdminAuthRequestStorable, userId: UserId): Promise<void> {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("User ID is required");
|
||||||
|
}
|
||||||
|
if (authRequest == null) {
|
||||||
|
throw new Error("Auth request is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, authRequest.toJSON(), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAdminAuthRequest(userId: UserId): Promise<void> {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("User ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId);
|
||||||
|
}
|
||||||
|
|
||||||
async approveOrDenyAuthRequest(
|
async approveOrDenyAuthRequest(
|
||||||
approve: boolean,
|
approve: boolean,
|
||||||
authRequest: AuthRequestResponse,
|
authRequest: AuthRequestResponse,
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { Utils } from "../../../platform/misc/utils";
|
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 {
|
export class AdminAuthRequestStorable {
|
||||||
id: string;
|
id: string;
|
||||||
privateKey: Uint8Array;
|
privateKey: Uint8Array;
|
||||||
|
@ -23,7 +19,7 @@ export class AdminAuthRequestStorable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJSON(obj: any): AdminAuthRequestStorable {
|
static fromJSON(obj: Jsonify<AdminAuthRequestStorable>): AdminAuthRequestStorable {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||||
|
@ -124,11 +123,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
|
|
||||||
setAdminAuthRequest: (
|
|
||||||
adminAuthRequest: AdminAuthRequestStorable,
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<void>;
|
|
||||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||||
|
@ -207,7 +201,5 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||||
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>;
|
|
||||||
setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>;
|
|
||||||
nextUpActiveUser: () => Promise<UserId>;
|
nextUpActiveUser: () => Promise<UserId>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
|
||||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
||||||
import {
|
import {
|
||||||
|
@ -169,7 +168,6 @@ export class AccountSettings {
|
||||||
protectedPin?: string;
|
protectedPin?: string;
|
||||||
vaultTimeout?: number;
|
vaultTimeout?: number;
|
||||||
vaultTimeoutAction?: string = "lock";
|
vaultTimeoutAction?: string = "lock";
|
||||||
approveLoginRequests?: boolean;
|
|
||||||
|
|
||||||
/** @deprecated July 2023, left for migration purposes*/
|
/** @deprecated July 2023, left for migration purposes*/
|
||||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||||
|
@ -206,7 +204,6 @@ export class Account {
|
||||||
profile?: AccountProfile = new AccountProfile();
|
profile?: AccountProfile = new AccountProfile();
|
||||||
settings?: AccountSettings = new AccountSettings();
|
settings?: AccountSettings = new AccountSettings();
|
||||||
tokens?: AccountTokens = new AccountTokens();
|
tokens?: AccountTokens = new AccountTokens();
|
||||||
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
|
|
||||||
|
|
||||||
constructor(init: Partial<Account>) {
|
constructor(init: Partial<Account>) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
|
@ -230,7 +227,6 @@ export class Account {
|
||||||
...new AccountTokens(),
|
...new AccountTokens(),
|
||||||
...init?.tokens,
|
...init?.tokens,
|
||||||
},
|
},
|
||||||
adminAuthRequest: init?.adminAuthRequest,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +241,6 @@ export class Account {
|
||||||
profile: AccountProfile.fromJSON(json?.profile),
|
profile: AccountProfile.fromJSON(json?.profile),
|
||||||
settings: AccountSettings.fromJSON(json?.settings),
|
settings: AccountSettings.fromJSON(json?.settings),
|
||||||
tokens: AccountTokens.fromJSON(json?.tokens),
|
tokens: AccountTokens.fromJSON(json?.tokens),
|
||||||
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest";
|
||||||
|
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { TokenService } from "../../auth/abstractions/token.service";
|
import { TokenService } from "../../auth/abstractions/token.service";
|
||||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||||
|
@ -548,37 +547,6 @@ export class StateService<
|
||||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
: await this.secureStorageService.save(DDG_SHARED_KEY, value, 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?.toJSON();
|
|
||||||
|
|
||||||
await this.saveAccount(account, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEmail(options?: StorageOptions): Promise<string> {
|
async getEmail(options?: StorageOptions): Promise<string> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
|
@ -1032,24 +1000,6 @@ export class StateService<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApproveLoginRequests(options?: StorageOptions): Promise<boolean> {
|
|
||||||
const approveLoginRequests = (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
|
||||||
)?.settings?.approveLoginRequests;
|
|
||||||
return approveLoginRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setApproveLoginRequests(value: boolean, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
account.settings.approveLoginRequests = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
||||||
let globals: TGlobalState;
|
let globals: TGlobalState;
|
||||||
if (this.useMemory(options.storageLocation)) {
|
if (this.useMemory(options.storageLocation)) {
|
||||||
|
@ -1392,7 +1342,6 @@ export class StateService<
|
||||||
protected resetAccount(account: TAccount) {
|
protected resetAccount(account: TAccount) {
|
||||||
const persistentAccountInformation = {
|
const persistentAccountInformation = {
|
||||||
settings: account.settings,
|
settings: account.settings,
|
||||||
adminAuthRequest: account.adminAuthRequest,
|
|
||||||
};
|
};
|
||||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,9 @@ export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||||
|
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
|
||||||
|
web: "disk-local",
|
||||||
|
});
|
||||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||||
export const TOKEN_DISK = new StateDefinition("token", "disk");
|
export const TOKEN_DISK = new StateDefinition("token", "disk");
|
||||||
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as signalR from "@microsoft/signalr";
|
||||||
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { AuthRequestServiceAbstraction } from "../../../auth/src/common/abstractions";
|
||||||
import { ApiService } from "../abstractions/api.service";
|
import { ApiService } from "../abstractions/api.service";
|
||||||
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
||||||
import { AuthService } from "../auth/abstractions/auth.service";
|
import { AuthService } from "../auth/abstractions/auth.service";
|
||||||
|
@ -18,6 +19,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service
|
||||||
import { LogService } from "../platform/abstractions/log.service";
|
import { LogService } from "../platform/abstractions/log.service";
|
||||||
import { MessagingService } from "../platform/abstractions/messaging.service";
|
import { MessagingService } from "../platform/abstractions/messaging.service";
|
||||||
import { StateService } from "../platform/abstractions/state.service";
|
import { StateService } from "../platform/abstractions/state.service";
|
||||||
|
import { UserId } from "../types/guid";
|
||||||
import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
export class NotificationsService implements NotificationsServiceAbstraction {
|
export class NotificationsService implements NotificationsServiceAbstraction {
|
||||||
|
@ -37,6 +39,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
) {
|
) {
|
||||||
this.environmentService.environment$.subscribe(() => {
|
this.environmentService.environment$.subscribe(() => {
|
||||||
|
@ -199,10 +202,13 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||||
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
||||||
break;
|
break;
|
||||||
case NotificationType.AuthRequest:
|
case NotificationType.AuthRequest:
|
||||||
if (await this.stateService.getApproveLoginRequests()) {
|
{
|
||||||
this.messagingService.send("openLoginApproval", {
|
const userId = await this.stateService.getUserId();
|
||||||
notificationId: notification.payload.id,
|
if (await this.authRequestService.getAcceptAuthRequests(userId as UserId)) {
|
||||||
});
|
this.messagingService.send("openLoginApproval", {
|
||||||
|
notificationId: notification.payload.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -52,6 +52,7 @@ import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version
|
||||||
import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers";
|
import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers";
|
||||||
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
|
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
|
||||||
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
|
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
|
||||||
|
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
|
||||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||||
|
@ -59,7 +60,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 55;
|
export const CURRENT_VERSION = 56;
|
||||||
|
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
|
@ -117,7 +118,8 @@ export function createMigrationBuilder() {
|
||||||
.with(DeleteInstalledVersion, 51, 52)
|
.with(DeleteInstalledVersion, 51, 52)
|
||||||
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
|
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
|
||||||
.with(SendMigrator, 53, 54)
|
.with(SendMigrator, 53, 54)
|
||||||
.with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION);
|
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
|
||||||
|
.with(AuthRequestMigrator, 55, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { AuthRequestMigrator } from "./56-move-auth-requests";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||||
|
FirstAccount: {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
approveLoginRequests: true,
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
adminAuthRequest: {
|
||||||
|
id: "id1",
|
||||||
|
privateKey: "privateKey1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SecondAccount: {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
user_FirstAccount_authRequestLocal_adminAuthRequest: {
|
||||||
|
id: "id1",
|
||||||
|
privateKey: "privateKey1",
|
||||||
|
},
|
||||||
|
user_FirstAccount_authRequestLocal_acceptAuthRequests: true,
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||||
|
FirstAccount: {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
SecondAccount: {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "authRequestLocal",
|
||||||
|
},
|
||||||
|
key: "adminAuthRequest",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "authRequestLocal",
|
||||||
|
},
|
||||||
|
key: "acceptAuthRequests",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("AuthRequestMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: AuthRequestMigrator;
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 55);
|
||||||
|
sut = new AuthRequestMigrator(55, 56);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the existing adminAuthRequest and approveLoginRequests", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("SecondAccount");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the adminAuthRequest and approveLoginRequests under the new key definitions", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, {
|
||||||
|
id: "id1",
|
||||||
|
privateKey: "privateKey1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, true);
|
||||||
|
expect(helper.setToUser).not.toHaveBeenCalledWith("SecondAccount");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 56);
|
||||||
|
sut = new AuthRequestMigrator(55, 56);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nulls the new adminAuthRequest and acceptAuthRequests values", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, null);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets back the adminAuthRequest and approveLoginRequests under old account object", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||||
|
adminAuthRequest: {
|
||||||
|
id: "id1",
|
||||||
|
privateKey: "privateKey1",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
approveLoginRequests: true,
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type AdminAuthRequestStorable = {
|
||||||
|
id: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpectedAccountType = {
|
||||||
|
adminAuthRequest?: AdminAuthRequestStorable;
|
||||||
|
settings?: {
|
||||||
|
approveLoginRequests?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "authRequestLocal",
|
||||||
|
},
|
||||||
|
key: "adminAuthRequest",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "authRequestLocal",
|
||||||
|
},
|
||||||
|
key: "acceptAuthRequests",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AuthRequestMigrator extends Migrator<55, 56> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
let updatedAccount = false;
|
||||||
|
|
||||||
|
// Migrate admin auth request
|
||||||
|
const existingAdminAuthRequest = account?.adminAuthRequest;
|
||||||
|
|
||||||
|
if (existingAdminAuthRequest != null) {
|
||||||
|
await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, existingAdminAuthRequest);
|
||||||
|
delete account.adminAuthRequest;
|
||||||
|
updatedAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate approve login requests
|
||||||
|
const existingApproveLoginRequests = account?.settings?.approveLoginRequests;
|
||||||
|
|
||||||
|
if (existingApproveLoginRequests != null) {
|
||||||
|
await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, existingApproveLoginRequests);
|
||||||
|
delete account.settings.approveLoginRequests;
|
||||||
|
updatedAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAccount) {
|
||||||
|
// Save the migrated account
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||||
|
|
||||||
|
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||||
|
let updatedAccount = false;
|
||||||
|
// Rollback admin auth request
|
||||||
|
const migratedAdminAuthRequest: AdminAuthRequestStorable = await helper.getFromUser(
|
||||||
|
userId,
|
||||||
|
ADMIN_AUTH_REQUEST_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (migratedAdminAuthRequest != null) {
|
||||||
|
account.adminAuthRequest = migratedAdminAuthRequest;
|
||||||
|
updatedAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, null);
|
||||||
|
|
||||||
|
// Rollback approve login requests
|
||||||
|
const migratedAcceptAuthRequest: boolean = await helper.getFromUser(
|
||||||
|
userId,
|
||||||
|
ACCEPT_AUTH_REQUESTS_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (migratedAcceptAuthRequest != null) {
|
||||||
|
account.settings = Object.assign(account.settings ?? {}, {
|
||||||
|
approveLoginRequests: migratedAcceptAuthRequest,
|
||||||
|
});
|
||||||
|
updatedAccount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, null);
|
||||||
|
|
||||||
|
if (updatedAccount) {
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue