mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[PM-7564] Move 2fa and login strategy service to popup and add state providers to 2fa service (#8820)
* remove 2fa from main.background * remove login strategy service from main.background * move 2fa and login strategy service to popup, init in browser * add state providers to 2fa service - add deserializer helpers * use key definitions for global state * fix calls to 2fa service * remove extra await * add delay to wait for active account emission in popup * add and fix tests * fix cli * really fix cli * remove timeout and wait for active account * verify expected user is active account * fix tests * address feedback
This commit is contained in:
parent
cbf7c292f3
commit
8afe915be1
@ -1,11 +1,13 @@
|
|||||||
import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||||
|
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FactoryOptions,
|
FactoryOptions,
|
||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
} from "../../../platform/background/service-factories/factory-options";
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory";
|
||||||
import {
|
import {
|
||||||
I18nServiceInitOptions,
|
I18nServiceInitOptions,
|
||||||
i18nServiceFactory,
|
i18nServiceFactory,
|
||||||
@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions;
|
|||||||
|
|
||||||
export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions &
|
export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions &
|
||||||
I18nServiceInitOptions &
|
I18nServiceInitOptions &
|
||||||
PlatformUtilsServiceInitOptions;
|
PlatformUtilsServiceInitOptions &
|
||||||
|
GlobalStateProvider;
|
||||||
|
|
||||||
export async function twoFactorServiceFactory(
|
export async function twoFactorServiceFactory(
|
||||||
cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices,
|
cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices,
|
||||||
@ -33,6 +36,7 @@ export async function twoFactorServiceFactory(
|
|||||||
new TwoFactorService(
|
new TwoFactorService(
|
||||||
await i18nServiceFactory(cache, opts),
|
await i18nServiceFactory(cache, opts),
|
||||||
await platformUtilsServiceFactory(cache, opts),
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
|
await globalStateProviderFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
service.init();
|
service.init();
|
||||||
|
@ -2,7 +2,10 @@ import { Component } from "@angular/core";
|
|||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
|
||||||
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
|
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import {
|
||||||
|
TwoFactorProviderDetails,
|
||||||
|
TwoFactorService,
|
||||||
|
} from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
|||||||
this.navigateTo2FA();
|
this.navigateTo2FA();
|
||||||
}
|
}
|
||||||
|
|
||||||
choose(p: any) {
|
override async choose(p: TwoFactorProviderDetails) {
|
||||||
super.choose(p);
|
await super.choose(p);
|
||||||
this.twoFactorService.setSelectedProvider(p.type);
|
await this.twoFactorService.setSelectedProvider(p.type);
|
||||||
|
|
||||||
this.navigateTo2FA();
|
this.navigateTo2FA();
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import { Subject, firstValueFrom, merge } from "rxjs";
|
|||||||
import {
|
import {
|
||||||
PinCryptoServiceAbstraction,
|
PinCryptoServiceAbstraction,
|
||||||
PinCryptoService,
|
PinCryptoService,
|
||||||
LoginStrategyServiceAbstraction,
|
|
||||||
LoginStrategyService,
|
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
UserDecryptionOptionsService,
|
UserDecryptionOptionsService,
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
@ -38,7 +36,6 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
|
|||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
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 { 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 { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@ -54,7 +51,6 @@ import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connect
|
|||||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
|
||||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||||
import {
|
import {
|
||||||
@ -277,7 +273,6 @@ export default class MainBackground {
|
|||||||
containerService: ContainerService;
|
containerService: ContainerService;
|
||||||
auditService: AuditServiceAbstraction;
|
auditService: AuditServiceAbstraction;
|
||||||
authService: AuthServiceAbstraction;
|
authService: AuthServiceAbstraction;
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction;
|
|
||||||
loginEmailService: LoginEmailServiceAbstraction;
|
loginEmailService: LoginEmailServiceAbstraction;
|
||||||
importApiService: ImportApiServiceAbstraction;
|
importApiService: ImportApiServiceAbstraction;
|
||||||
importService: ImportServiceAbstraction;
|
importService: ImportServiceAbstraction;
|
||||||
@ -301,7 +296,6 @@ export default class MainBackground {
|
|||||||
providerService: ProviderServiceAbstraction;
|
providerService: ProviderServiceAbstraction;
|
||||||
keyConnectorService: KeyConnectorServiceAbstraction;
|
keyConnectorService: KeyConnectorServiceAbstraction;
|
||||||
userVerificationService: UserVerificationServiceAbstraction;
|
userVerificationService: UserVerificationServiceAbstraction;
|
||||||
twoFactorService: TwoFactorServiceAbstraction;
|
|
||||||
vaultFilterService: VaultFilterService;
|
vaultFilterService: VaultFilterService;
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
||||||
encryptService: EncryptService;
|
encryptService: EncryptService;
|
||||||
@ -614,8 +608,6 @@ export default class MainBackground {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||||
|
|
||||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||||
@ -659,32 +651,6 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.loginEmailService = new LoginEmailService(this.stateProvider);
|
this.loginEmailService = new LoginEmailService(this.stateProvider);
|
||||||
|
|
||||||
this.loginStrategyService = new LoginStrategyService(
|
|
||||||
this.accountService,
|
|
||||||
this.masterPasswordService,
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.keyConnectorService,
|
|
||||||
this.environmentService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this.i18nService,
|
|
||||||
this.encryptService,
|
|
||||||
this.passwordStrengthService,
|
|
||||||
this.policyService,
|
|
||||||
this.deviceTrustService,
|
|
||||||
this.authRequestService,
|
|
||||||
this.userDecryptionOptionsService,
|
|
||||||
this.globalStateProvider,
|
|
||||||
this.billingAccountProfileStateService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
||||||
|
|
||||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||||
@ -1114,8 +1080,7 @@ export default class MainBackground {
|
|||||||
this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
|
this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
|
||||||
|
|
||||||
await (this.i18nService as I18nService).init();
|
await (this.i18nService as I18nService).init();
|
||||||
await (this.eventUploadService as EventUploadService).init(true);
|
(this.eventUploadService as EventUploadService).init(true);
|
||||||
this.twoFactorService.init();
|
|
||||||
|
|
||||||
if (this.popupOnlyContext) {
|
if (this.popupOnlyContext) {
|
||||||
return;
|
return;
|
||||||
|
@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common";
|
|||||||
import { Inject, Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -15,6 +16,7 @@ export class InitService {
|
|||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private stateService: StateServiceAbstraction,
|
private stateService: StateServiceAbstraction,
|
||||||
|
private twoFactorService: TwoFactorService,
|
||||||
private logService: LogServiceAbstraction,
|
private logService: LogServiceAbstraction,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
@ -24,6 +26,7 @@ export class InitService {
|
|||||||
return async () => {
|
return async () => {
|
||||||
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
||||||
await this.i18nService.init();
|
await this.i18nService.init();
|
||||||
|
this.twoFactorService.init();
|
||||||
|
|
||||||
if (!BrowserPopupUtils.inPopup(window)) {
|
if (!BrowserPopupUtils.inPopup(window)) {
|
||||||
window.document.body.classList.add("body-full");
|
window.document.body.classList.add("body-full");
|
||||||
|
@ -15,10 +15,7 @@ import {
|
|||||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import {
|
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
AuthRequestServiceAbstraction,
|
|
||||||
LoginStrategyServiceAbstraction,
|
|
||||||
} from "@bitwarden/auth/common";
|
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||||
@ -33,7 +30,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d
|
|||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import {
|
import {
|
||||||
@ -168,21 +164,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: UnauthGuardService,
|
useClass: UnauthGuardService,
|
||||||
deps: [AuthServiceAbstraction, Router],
|
deps: [AuthServiceAbstraction, Router],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: TwoFactorService,
|
|
||||||
useFactory: getBgService<TwoFactorService>("twoFactorService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AuthServiceAbstraction,
|
provide: AuthServiceAbstraction,
|
||||||
useFactory: getBgService<AuthService>("authService"),
|
useFactory: getBgService<AuthService>("authService"),
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: LoginStrategyServiceAbstraction,
|
|
||||||
useFactory: getBgService<LoginStrategyServiceAbstraction>("loginStrategyService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SsoLoginServiceAbstraction,
|
provide: SsoLoginServiceAbstraction,
|
||||||
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
|
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
|
||||||
|
@ -231,7 +231,7 @@ export class LoginCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response.requiresTwoFactor) {
|
if (response.requiresTwoFactor) {
|
||||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
|
||||||
if (twoFactorProviders.length === 0) {
|
if (twoFactorProviders.length === 0) {
|
||||||
return Response.badRequest("No providers available for this client.");
|
return Response.badRequest("No providers available for this client.");
|
||||||
}
|
}
|
||||||
@ -272,7 +272,7 @@ export class LoginCommand {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
twoFactorToken == null &&
|
twoFactorToken == null &&
|
||||||
response.twoFactorProviders.size > 1 &&
|
Object.keys(response.twoFactorProviders).length > 1 &&
|
||||||
selectedProvider.type === TwoFactorProviderType.Email
|
selectedProvider.type === TwoFactorProviderType.Email
|
||||||
) {
|
) {
|
||||||
const emailReq = new TwoFactorEmailRequest();
|
const emailReq = new TwoFactorEmailRequest();
|
||||||
|
@ -455,7 +455,11 @@ export class Main {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
this.twoFactorService = new TwoFactorService(
|
||||||
|
this.i18nService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.globalStateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
this.passwordStrengthService = new PasswordStrengthService();
|
this.passwordStrengthService = new PasswordStrengthService();
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ describe("SsoComponent", () => {
|
|||||||
describe("2FA scenarios", () => {
|
describe("2FA scenarios", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const authResult = new AuthResult();
|
const authResult = new AuthResult();
|
||||||
authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]);
|
authResult.twoFactorProviders = { [TwoFactorProviderType.Authenticator]: {} };
|
||||||
|
|
||||||
// use standard user with MP because this test is not concerned with password reset.
|
// use standard user with MP because this test is not concerned with password reset.
|
||||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||||
|
@ -2,7 +2,10 @@ import { Directive, EventEmitter, OnInit, Output } from "@angular/core";
|
|||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import {
|
||||||
|
TwoFactorProviderDetails,
|
||||||
|
TwoFactorService,
|
||||||
|
} from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -24,11 +27,11 @@ export class TwoFactorOptionsComponent implements OnInit {
|
|||||||
protected environmentService: EnvironmentService,
|
protected environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
this.providers = this.twoFactorService.getSupportedProviders(this.win);
|
this.providers = await this.twoFactorService.getSupportedProviders(this.win);
|
||||||
}
|
}
|
||||||
|
|
||||||
choose(p: any) {
|
async choose(p: TwoFactorProviderDetails) {
|
||||||
this.onProviderSelected.emit(p.type);
|
this.onProviderSelected.emit(p.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (!(await this.authing()) || this.twoFactorService.getProviders() == null) {
|
if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) {
|
||||||
// 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.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate([this.loginRoute]);
|
this.router.navigate([this.loginRoute]);
|
||||||
@ -145,7 +145,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported);
|
this.selectedProviderType = await this.twoFactorService.getDefaultProvider(
|
||||||
|
this.webAuthnSupported,
|
||||||
|
);
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,12 +164,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
|
|
||||||
this.cleanupWebAuthn();
|
this.cleanupWebAuthn();
|
||||||
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
||||||
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||||
|
return providers.get(this.selectedProviderType);
|
||||||
|
});
|
||||||
switch (this.selectedProviderType) {
|
switch (this.selectedProviderType) {
|
||||||
case TwoFactorProviderType.WebAuthn:
|
case TwoFactorProviderType.WebAuthn:
|
||||||
if (!this.webAuthnNewTab) {
|
if (!this.webAuthnNewTab) {
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
this.authWebAuthn();
|
await this.authWebAuthn();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -212,7 +216,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.Email:
|
case TwoFactorProviderType.Email:
|
||||||
this.twoFactorEmail = providerData.Email;
|
this.twoFactorEmail = providerData.Email;
|
||||||
if (this.twoFactorService.getProviders().size > 1) {
|
if ((await this.twoFactorService.getProviders()).size > 1) {
|
||||||
await this.sendEmail(false);
|
await this.sendEmail(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -474,8 +478,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
this.emailPromise = null;
|
this.emailPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
authWebAuthn() {
|
async authWebAuthn() {
|
||||||
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||||
|
return providers.get(this.selectedProviderType);
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.webAuthnSupported || this.webAuthn == null) {
|
if (!this.webAuthnSupported || this.webAuthn == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -874,7 +874,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: TwoFactorServiceAbstraction,
|
provide: TwoFactorServiceAbstraction,
|
||||||
useClass: TwoFactorService,
|
useClass: TwoFactorService,
|
||||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: FormValidationErrorsServiceAbstraction,
|
provide: FormValidationErrorsServiceAbstraction,
|
||||||
|
@ -86,7 +86,9 @@ describe("AuthRequestLoginStrategy", () => {
|
|||||||
|
|
||||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({});
|
tokenService.decodeAccessToken.mockResolvedValue({
|
||||||
|
sub: mockUserId,
|
||||||
|
});
|
||||||
|
|
||||||
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
@ -25,11 +25,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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import {
|
import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account";
|
||||||
Account,
|
|
||||||
AccountProfile,
|
|
||||||
AccountKeys,
|
|
||||||
} from "@bitwarden/common/platform/models/domain/account";
|
|
||||||
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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
@ -214,7 +210,6 @@ describe("LoginStrategy", () => {
|
|||||||
email: email,
|
email: email,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keys: new AccountKeys(),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||||
@ -223,6 +218,21 @@ describe("LoginStrategy", () => {
|
|||||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("throws if active account isn't found after being initialized", async () => {
|
||||||
|
const idTokenResponse = identityTokenResponseFactory();
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||||
|
|
||||||
|
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
|
||||||
|
const mockVaultTimeout = 1000;
|
||||||
|
|
||||||
|
stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction);
|
||||||
|
stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout);
|
||||||
|
|
||||||
|
accountService.activeAccountSubject.next(null);
|
||||||
|
|
||||||
|
await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("builds AuthResult", async () => {
|
it("builds AuthResult", async () => {
|
||||||
const tokenResponse = identityTokenResponseFactory();
|
const tokenResponse = identityTokenResponseFactory();
|
||||||
tokenResponse.forcePasswordReset = true;
|
tokenResponse.forcePasswordReset = true;
|
||||||
@ -306,8 +316,10 @@ describe("LoginStrategy", () => {
|
|||||||
expect(tokenService.clearTwoFactorToken).toHaveBeenCalled();
|
expect(tokenService.clearTwoFactorToken).toHaveBeenCalled();
|
||||||
|
|
||||||
const expected = new AuthResult();
|
const expected = new AuthResult();
|
||||||
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
expected.twoFactorProviders = { 0: null } as Record<
|
||||||
expected.twoFactorProviders.set(0, null);
|
TwoFactorProviderType,
|
||||||
|
Record<string, string>
|
||||||
|
>;
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -336,8 +348,9 @@ describe("LoginStrategy", () => {
|
|||||||
expect(messagingService.send).not.toHaveBeenCalled();
|
expect(messagingService.send).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const expected = new AuthResult();
|
const expected = new AuthResult();
|
||||||
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
expected.twoFactorProviders = {
|
||||||
expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" });
|
[TwoFactorProviderType.Email]: { Email: "k***@bitwarden.com" },
|
||||||
|
};
|
||||||
expected.email = userEmail;
|
expected.email = userEmail;
|
||||||
expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken;
|
expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs";
|
||||||
|
|
||||||
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";
|
||||||
@ -101,7 +101,7 @@ export abstract class LoginStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
|
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
|
||||||
this.twoFactorService.clearSelectedProvider();
|
await this.twoFactorService.clearSelectedProvider();
|
||||||
|
|
||||||
const tokenRequest = this.cache.value.tokenRequest;
|
const tokenRequest = this.cache.value.tokenRequest;
|
||||||
const response = await this.apiService.postIdentityToken(tokenRequest);
|
const response = await this.apiService.postIdentityToken(tokenRequest);
|
||||||
@ -159,12 +159,12 @@ export abstract class LoginStrategy {
|
|||||||
* It also sets the access token and refresh token in the token service.
|
* It also sets the access token and refresh token in the token service.
|
||||||
*
|
*
|
||||||
* @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<UserId>} - A promise that resolves the the UserId when the account information has been successfully saved.
|
||||||
*/
|
*/
|
||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
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 as UserId;
|
||||||
|
|
||||||
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId });
|
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId });
|
||||||
const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
|
const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
|
||||||
@ -191,6 +191,8 @@ export abstract class LoginStrategy {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.verifyAccountAdded(userId);
|
||||||
|
|
||||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(
|
await this.userDecryptionOptionsService.setUserDecryptionOptions(
|
||||||
UserDecryptionOptions.fromResponse(tokenResponse),
|
UserDecryptionOptions.fromResponse(tokenResponse),
|
||||||
);
|
);
|
||||||
@ -207,7 +209,7 @@ export abstract class LoginStrategy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
|
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
|
||||||
return userId as UserId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
||||||
@ -284,7 +286,7 @@ export abstract class LoginStrategy {
|
|||||||
const result = new AuthResult();
|
const result = new AuthResult();
|
||||||
result.twoFactorProviders = response.twoFactorProviders2;
|
result.twoFactorProviders = response.twoFactorProviders2;
|
||||||
|
|
||||||
this.twoFactorService.setProviders(response);
|
await this.twoFactorService.setProviders(response);
|
||||||
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
|
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
|
||||||
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
|
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
|
||||||
result.email = response.email;
|
result.email = response.email;
|
||||||
@ -306,4 +308,24 @@ export abstract class LoginStrategy {
|
|||||||
result.captchaSiteKey = response.siteKey;
|
result.captchaSiteKey = response.siteKey;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the active account is set after initialization.
|
||||||
|
* Note: In browser there is a slight delay between when active account emits in background,
|
||||||
|
* and when it emits in foreground. We're giving the foreground 1 second to catch up.
|
||||||
|
* If nothing is emitted, we throw an error.
|
||||||
|
*/
|
||||||
|
private async verifyAccountAdded(expectedUserId: UserId) {
|
||||||
|
await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(
|
||||||
|
filter((account) => account?.id === expectedUserId),
|
||||||
|
timeout({
|
||||||
|
first: 1000,
|
||||||
|
with: () => {
|
||||||
|
throw new Error("Expected user never made active user after initialization.");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,9 @@ describe("PasswordLoginStrategy", () => {
|
|||||||
kdfConfigService = mock<KdfConfigService>();
|
kdfConfigService = mock<KdfConfigService>();
|
||||||
|
|
||||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({});
|
tokenService.decodeAccessToken.mockResolvedValue({
|
||||||
|
sub: userId,
|
||||||
|
});
|
||||||
|
|
||||||
loginStrategyService.makePreloginKey.mockResolvedValue(masterKey);
|
loginStrategyService.makePreloginKey.mockResolvedValue(masterKey);
|
||||||
|
|
||||||
|
@ -92,7 +92,9 @@ describe("SsoLoginStrategy", () => {
|
|||||||
|
|
||||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({});
|
tokenService.decodeAccessToken.mockResolvedValue({
|
||||||
|
sub: userId,
|
||||||
|
});
|
||||||
|
|
||||||
ssoLoginStrategy = new SsoLoginStrategy(
|
ssoLoginStrategy = new SsoLoginStrategy(
|
||||||
null,
|
null,
|
||||||
|
@ -82,7 +82,9 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
|
|
||||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({});
|
tokenService.decodeAccessToken.mockResolvedValue({
|
||||||
|
sub: userId,
|
||||||
|
});
|
||||||
|
|
||||||
apiLogInStrategy = new UserApiLoginStrategy(
|
apiLogInStrategy = new UserApiLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
@ -18,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.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 { FakeAccountService } from "@bitwarden/common/spec";
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||||
@ -49,6 +50,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||||||
|
|
||||||
const token = "mockToken";
|
const token = "mockToken";
|
||||||
const deviceId = Utils.newGuid();
|
const deviceId = Utils.newGuid();
|
||||||
|
const userId = Utils.newGuid() as UserId;
|
||||||
|
|
||||||
let webAuthnCredentials!: WebAuthnLoginCredentials;
|
let webAuthnCredentials!: WebAuthnLoginCredentials;
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
accountService = new FakeAccountService(null);
|
accountService = mockAccountServiceWith(userId);
|
||||||
masterPasswordService = new FakeMasterPasswordService();
|
masterPasswordService = new FakeMasterPasswordService();
|
||||||
|
|
||||||
cryptoService = mock<CryptoService>();
|
cryptoService = mock<CryptoService>();
|
||||||
@ -87,7 +89,9 @@ describe("WebAuthnLoginStrategy", () => {
|
|||||||
|
|
||||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({});
|
tokenService.decodeAccessToken.mockResolvedValue({
|
||||||
|
sub: userId,
|
||||||
|
});
|
||||||
|
|
||||||
webAuthnLoginStrategy = new WebAuthnLoginStrategy(
|
webAuthnLoginStrategy = new WebAuthnLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
@ -12,12 +12,12 @@ export interface TwoFactorProviderDetails {
|
|||||||
|
|
||||||
export abstract class TwoFactorService {
|
export abstract class TwoFactorService {
|
||||||
init: () => void;
|
init: () => void;
|
||||||
getSupportedProviders: (win: Window) => TwoFactorProviderDetails[];
|
getSupportedProviders: (win: Window) => Promise<TwoFactorProviderDetails[]>;
|
||||||
getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
|
getDefaultProvider: (webAuthnSupported: boolean) => Promise<TwoFactorProviderType>;
|
||||||
setSelectedProvider: (type: TwoFactorProviderType) => void;
|
setSelectedProvider: (type: TwoFactorProviderType) => Promise<void>;
|
||||||
clearSelectedProvider: () => void;
|
clearSelectedProvider: () => Promise<void>;
|
||||||
|
|
||||||
setProviders: (response: IdentityTwoFactorResponse) => void;
|
setProviders: (response: IdentityTwoFactorResponse) => Promise<void>;
|
||||||
clearProviders: () => void;
|
clearProviders: () => Promise<void>;
|
||||||
getProviders: () => Map<TwoFactorProviderType, { [key: string]: string }>;
|
getProviders: () => Promise<Map<TwoFactorProviderType, { [key: string]: string }>>;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ export class AuthResult {
|
|||||||
resetMasterPassword = false;
|
resetMasterPassword = false;
|
||||||
|
|
||||||
forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
twoFactorProviders: Partial<Record<TwoFactorProviderType, Record<string, string>>> = null;
|
||||||
ssoEmail2FaSessionToken?: string;
|
ssoEmail2FaSessionToken?: string;
|
||||||
email: string;
|
email: string;
|
||||||
requiresEncryptionKeyMigration: boolean;
|
requiresEncryptionKeyMigration: boolean;
|
||||||
|
@ -4,8 +4,10 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
|||||||
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
|
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
|
||||||
|
|
||||||
export class IdentityTwoFactorResponse extends BaseResponse {
|
export class IdentityTwoFactorResponse extends BaseResponse {
|
||||||
|
// contains available two-factor providers
|
||||||
twoFactorProviders: TwoFactorProviderType[];
|
twoFactorProviders: TwoFactorProviderType[];
|
||||||
twoFactorProviders2 = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
// a map of two-factor providers to necessary data for completion
|
||||||
|
twoFactorProviders2: Record<TwoFactorProviderType, Record<string, string>>;
|
||||||
captchaToken: string;
|
captchaToken: string;
|
||||||
ssoEmail2faSessionToken: string;
|
ssoEmail2faSessionToken: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@ -15,15 +17,7 @@ export class IdentityTwoFactorResponse extends BaseResponse {
|
|||||||
super(response);
|
super(response);
|
||||||
this.captchaToken = this.getResponseProperty("CaptchaBypassToken");
|
this.captchaToken = this.getResponseProperty("CaptchaBypassToken");
|
||||||
this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders");
|
this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders");
|
||||||
const twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2");
|
this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2");
|
||||||
if (twoFactorProviders2 != null) {
|
|
||||||
for (const prop in twoFactorProviders2) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (twoFactorProviders2.hasOwnProperty(prop)) {
|
|
||||||
this.twoFactorProviders2.set(parseInt(prop, null), twoFactorProviders2[prop]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
|
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
|
||||||
this.getResponseProperty("MasterPasswordPolicy"),
|
this.getResponseProperty("MasterPasswordPolicy"),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { GlobalStateProvider, KeyDefinition, TWO_FACTOR_MEMORY } from "../../platform/state";
|
||||||
import {
|
import {
|
||||||
TwoFactorProviderDetails,
|
TwoFactorProviderDetails,
|
||||||
TwoFactorService as TwoFactorServiceAbstraction,
|
TwoFactorService as TwoFactorServiceAbstraction,
|
||||||
@ -59,13 +63,36 @@ export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactor
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Memory storage as only required during authentication process
|
||||||
|
export const PROVIDERS = KeyDefinition.record<Record<string, string>, TwoFactorProviderType>(
|
||||||
|
TWO_FACTOR_MEMORY,
|
||||||
|
"providers",
|
||||||
|
{
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memory storage as only required during authentication process
|
||||||
|
export const SELECTED_PROVIDER = new KeyDefinition<TwoFactorProviderType>(
|
||||||
|
TWO_FACTOR_MEMORY,
|
||||||
|
"selected",
|
||||||
|
{
|
||||||
|
deserializer: (obj) => obj,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export class TwoFactorService implements TwoFactorServiceAbstraction {
|
export class TwoFactorService implements TwoFactorServiceAbstraction {
|
||||||
private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
|
private providersState = this.globalStateProvider.get(PROVIDERS);
|
||||||
private selectedTwoFactorProviderType: TwoFactorProviderType = null;
|
private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER);
|
||||||
|
readonly providers$ = this.providersState.state$.pipe(
|
||||||
|
map((providers) => Utils.recordToMap(providers)),
|
||||||
|
);
|
||||||
|
readonly selected$ = this.selectedState.state$;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private globalStateProvider: GlobalStateProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -93,63 +120,60 @@ export class TwoFactorService implements TwoFactorServiceAbstraction {
|
|||||||
this.i18nService.t("yubiKeyDesc");
|
this.i18nService.t("yubiKeyDesc");
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
|
async getSupportedProviders(win: Window): Promise<TwoFactorProviderDetails[]> {
|
||||||
|
const data = await firstValueFrom(this.providers$);
|
||||||
const providers: any[] = [];
|
const providers: any[] = [];
|
||||||
if (this.twoFactorProvidersData == null) {
|
if (data == null) {
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) &&
|
data.has(TwoFactorProviderType.OrganizationDuo) &&
|
||||||
this.platformUtilsService.supportsDuo()
|
this.platformUtilsService.supportsDuo()
|
||||||
) {
|
) {
|
||||||
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
|
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) {
|
if (data.has(TwoFactorProviderType.Authenticator)) {
|
||||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
|
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) {
|
if (data.has(TwoFactorProviderType.Yubikey)) {
|
||||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
|
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) {
|
||||||
this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) &&
|
|
||||||
this.platformUtilsService.supportsDuo()
|
|
||||||
) {
|
|
||||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
|
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) &&
|
data.has(TwoFactorProviderType.WebAuthn) &&
|
||||||
this.platformUtilsService.supportsWebAuthn(win)
|
this.platformUtilsService.supportsWebAuthn(win)
|
||||||
) {
|
) {
|
||||||
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
|
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) {
|
if (data.has(TwoFactorProviderType.Email)) {
|
||||||
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
|
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
|
async getDefaultProvider(webAuthnSupported: boolean): Promise<TwoFactorProviderType> {
|
||||||
if (this.twoFactorProvidersData == null) {
|
const data = await firstValueFrom(this.providers$);
|
||||||
|
const selected = await firstValueFrom(this.selected$);
|
||||||
|
if (data == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (selected != null && data.has(selected)) {
|
||||||
this.selectedTwoFactorProviderType != null &&
|
return selected;
|
||||||
this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)
|
|
||||||
) {
|
|
||||||
return this.selectedTwoFactorProviderType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let providerType: TwoFactorProviderType = null;
|
let providerType: TwoFactorProviderType = null;
|
||||||
let providerPriority = -1;
|
let providerPriority = -1;
|
||||||
this.twoFactorProvidersData.forEach((_value, type) => {
|
data.forEach((_value, type) => {
|
||||||
const provider = (TwoFactorProviders as any)[type];
|
const provider = (TwoFactorProviders as any)[type];
|
||||||
if (provider != null && provider.priority > providerPriority) {
|
if (provider != null && provider.priority > providerPriority) {
|
||||||
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
|
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
|
||||||
@ -164,23 +188,23 @@ export class TwoFactorService implements TwoFactorServiceAbstraction {
|
|||||||
return providerType;
|
return providerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedProvider(type: TwoFactorProviderType) {
|
async setSelectedProvider(type: TwoFactorProviderType): Promise<void> {
|
||||||
this.selectedTwoFactorProviderType = type;
|
await this.selectedState.update(() => type);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSelectedProvider() {
|
async clearSelectedProvider(): Promise<void> {
|
||||||
this.selectedTwoFactorProviderType = null;
|
await this.selectedState.update(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProviders(response: IdentityTwoFactorResponse) {
|
async setProviders(response: IdentityTwoFactorResponse): Promise<void> {
|
||||||
this.twoFactorProvidersData = response.twoFactorProviders2;
|
await this.providersState.update(() => response.twoFactorProviders2);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearProviders() {
|
async clearProviders(): Promise<void> {
|
||||||
this.twoFactorProvidersData = null;
|
await this.providersState.update(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProviders() {
|
getProviders(): Promise<Map<TwoFactorProviderType, { [key: string]: string }>> {
|
||||||
return this.twoFactorProvidersData;
|
return firstValueFrom(this.providers$);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { record } from "./deserialization-helpers";
|
||||||
|
|
||||||
|
describe("deserialization helpers", () => {
|
||||||
|
describe("record", () => {
|
||||||
|
it("deserializes a record when keys are strings", () => {
|
||||||
|
const deserializer = record((value: number) => value);
|
||||||
|
const input = {
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
};
|
||||||
|
const output = deserializer(input);
|
||||||
|
expect(output).toEqual(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deserializes a record when keys are numbers", () => {
|
||||||
|
const deserializer = record((value: number) => value);
|
||||||
|
const input = {
|
||||||
|
1: 1,
|
||||||
|
2: 2,
|
||||||
|
};
|
||||||
|
const output = deserializer(input);
|
||||||
|
expect(output).toEqual(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -21,7 +21,7 @@ export function array<T>(
|
|||||||
*
|
*
|
||||||
* @param valueDeserializer
|
* @param valueDeserializer
|
||||||
*/
|
*/
|
||||||
export function record<T, TKey extends string = string>(
|
export function record<T, TKey extends string | number = string>(
|
||||||
valueDeserializer: (value: Jsonify<T>) => T,
|
valueDeserializer: (value: Jsonify<T>) => T,
|
||||||
): (record: Jsonify<Record<TKey, T>>) => Record<TKey, T> {
|
): (record: Jsonify<Record<TKey, T>>) => Record<TKey, T> {
|
||||||
return (jsonValue: Jsonify<Record<TKey, T> | null>) => {
|
return (jsonValue: Jsonify<Record<TKey, T> | null>) => {
|
||||||
@ -29,10 +29,10 @@ export function record<T, TKey extends string = string>(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const output: Record<string, T> = {};
|
const output: Record<TKey, T> = {} as any;
|
||||||
for (const key in jsonValue) {
|
Object.entries(jsonValue).forEach(([key, value]) => {
|
||||||
output[key] = valueDeserializer((jsonValue as Record<string, Jsonify<T>>)[key]);
|
output[key as TKey] = valueDeserializer(value);
|
||||||
}
|
});
|
||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ export class KeyDefinition<T> {
|
|||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
static record<T, TKey extends string = string>(
|
static record<T, TKey extends string | number = string>(
|
||||||
stateDefinition: StateDefinition,
|
stateDefinition: StateDefinition,
|
||||||
key: string,
|
key: string,
|
||||||
// We have them provide options for the value of the record, depending on future options we add, this could get a little weird.
|
// We have them provide options for the value of the record, depending on future options we add, this could get a little weird.
|
||||||
|
@ -40,6 +40,7 @@ export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
|||||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||||
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
|
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
|
||||||
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
||||||
|
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
||||||
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
||||||
export const ROUTER_DISK = new StateDefinition("router", "disk");
|
export const ROUTER_DISK = new StateDefinition("router", "disk");
|
||||||
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
||||||
|
@ -120,7 +120,7 @@ export class UserKeyDefinition<T> {
|
|||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
static record<T, TKey extends string = string>(
|
static record<T, TKey extends string | number = string>(
|
||||||
stateDefinition: StateDefinition,
|
stateDefinition: StateDefinition,
|
||||||
key: string,
|
key: string,
|
||||||
// We have them provide options for the value of the record, depending on future options we add, this could get a little weird.
|
// We have them provide options for the value of the record, depending on future options we add, this could get a little weird.
|
||||||
|
Loading…
Reference in New Issue
Block a user