mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[PM-8111] Browser Refresh: LoginComponent (#10856)
* setup new LoginComponent files in libs/auth * update pageTitle * handle loading email settings * setup web-login.service.ts * implement web onInit * fill out webOnInit * refactor getOrgPolicies call * update import * add validateEmail logic * handle registerRoute * add showPasswordless flag * handle captcha * handle startAuthRequestLogin() * add handleMigrateEncryptionKey to default and web service * handle submit routing (web) * fix typo * incorporate loginEmailService changes * minor updates to comments for clarity * create a defaultOnInit() * update defaultOnInit() * handle master password input focus * handle post-login routing on Browser/Desktop * handle browser/desktop syncService * handle browser ngOnInit * handle browser routing and basic browser template * setup desktop router * add template for desktop first UI state: email entry * rename 'response' to 'authResult' * refactor handleMigrateEncryptionKey() * refactor captcha methods and add return types * refactor submit logic * refactor submit logic further to use if statements with returns instead of if...else if...else * remove toast error on invalid form for Browser/Desktop * refactor to handleAuthResult() method * refactor webOnInit * add comment to revisit ngOnInit logic * refactor handlCaptchaRequired() * create a LoginSecondaryContentComponent for AnonLayout use * minor formatting for consistency * add clarifying comment to handleAuthResult() * minor refactor to use destructuring * setup desktopOnInit() * add continue() method * handle desktop ngOnDestroy() * add clarifying comment regarding secondary content * fill out desktop template and submit() * add descriptive comment to top of HTML file * refactor to use a uiState enum for UI states * handle oss-routing swap * handle registerRoute$ in secondary content * web template modifications * change email validation to only run on submit (or when clicking continue button) * add dynamic anon-layout wrapper data * remove static element ref * desktop HTML template updates * remove 'showPassword' property b/c now handled by bitPasswordInputToggle * Extension: setup EmailEntry state UI * Extension: setup MasterPasswordEntry state UI * ensure full sync happens on all clients before navigation * update icon stroke color * change old components to V1 * remove 'V2' from new component * update captcha iframe on all clients * add browser redirect from /home to /login with FF on * add todo comment regarding browser template * add launchSsoBrowser to extension template * move extension launchSsoBrowserWindow() to extension service * cleanup & comments * add launchSsoBrowserWindow() to default service * setup launchSsoBrowserWindow() for Desktop * refactor to use toastService * remove unnecessary service injection * rename LoginService to LoginComponentService to avoid confusion with the LoginStrategyService * add jsdocs to LoginComponentService * rename loginService prop to loginComponentService * Add vault icon to anon layout. * Prevent email address validation on blur. * Fix comment typo. * Prefill email field when "create account" is clicked. * Use factory function to provide LoginEmailService. * Add test for RegisterFormComponent. * Remove back button todo. * Consolidate clearing loginEmailService values and routing * Remove unnecessary navigation. * Fix client navigation after login. * Consolidate login templates. * Break up LoginComponent into client-specific services. * Rename login.component to login-v1.component * Rename login.component to login-v1.component * Revert "Rename login.component to login-v1.component" This reverts commit9a277d6ca5
. * Revert "Rename login.component to login-v1.component" This reverts commit588a7af906
. * Rename login.component to login-v1.component except browser. * Comment out debug code. * Remove debug code. * Rename login.component to login-v1.component for browser. * Add login-with-passkey route to desktop. * Set feature flag to false. * Fix linting errors. * Populate email on registration start form. * Implement email population on all clients add add safeProviders. * Remove comment re. passing email to registration. * Add unauthUiRefreshRedirect utility function. * Add transparent border. * Merge main and add satisfies RouteDataProperties * PM-8111 - Extension - AppRoutingModule - Home route now redirects conditionally based on unauthenticated ui refresh feature flag. * PM-8111 - New Login Comp + Login Comp Svc - (1) Refactor naming and returns of getShowPasswordlessFlag to isLoginViaAuthRequestSupported (2) Replace showPasswordless with better composed variable names. * PM-8111 - TODO cleanup * PM-8111 - (1) Cleanup DefaultLoginComponentService (2) Sso Connector now checks client id property instead of reading it from state * PM-8111 - Two TODO cleanups * Remove specific client services. * Add isLoginWithPasskeySupported function to reduce client type checking in template. * Add styles missing from Browser to Create Account link. * Confirmed inline form errors working and removing todo comments. * Convert refactoring todo-rr-bw to standard todos. * Add login component services tests. * Cleanup formatting and remove unused provider. * Add comment to explain call to setLoginEmail. * Rearrange imports to fix lint error. * Adjust styles for password hint link. * Address PR feedback: use strict comparison. * Ensure Login with Passkey button is shown by setting clientType. * Update "continue" button from "submit" to "button" type. * Ensure Passkey login available for web and desktop. * Validate email on enter keypress. * Use click event to trigger goToHint. * Restructure handAuthResult to ensure we redirect to vault. * Add await to saveEmailSettings function. * Directly set clientType in individual login component services. * Get clientType via service. * Add back button. * Remove hardcoded colors from Vault Icon * Removing register component changes. * Removing register component changes. * Ensure isLoginWithPasskeySupported is only returns true for web client. * Remove Web/Desktop comment from html template * Update Storybook with initialLoginEmail * Fix translation error * Add test for unauthUiRefreshRedirect. * Rename goAfterLogIn to evaluatePassword and borrow logic from lock component. * Add DefaultLoginComponent tests. * Integrate changes to translations. * Simplify ngOnInit: remove webOnInit and move getLoginWithDevice to defaultOnInit I couldn't find any usages of qParams.org or qParams.sponsorshipToken on QA (signing up for family membership, creating organization, manually modifying query params), so I think these are safe to remove. * Fix translations. * Clean up and flush out register form tests. * Update variable name. * Remove unused enforcedPasswordPolicyOptions property. * Run prettier. * Add back safeProviders for LoginEmailService * Remove duplicate import. * Update v1 web login title. * Adjust overlay position of EnvironmentSelectorComponent for new layout. Since the switcher is located at the bottom of the screen we need to position it up above the trigger button so that it is not cut off. * Add new wave icon * Only send email in query parameters if set. * Remove test/debug code. * Replace loggedEmail with this.emailFormControl.value. * Move getLoginWithDevice call to loadEmailSettings. * Replace loggedEmail with this.emailFormControl.value. * Add todo comment re. inline errors. * Remove unused setPreviousUrl function. * Remove height / width from vault icon svg. * Use continue method unanimously * WIP remove validated email& display extension back button * Simplify getting query params * Rework ExtensionAnonLayoutWrapperDataService to use BehaviorSubject * Simplify validateEmail method * Hide back button on init * Revert "Hide back button on init" This reverts commite8de5e2bfc
. * Revert "Simplify validateEmail method" This reverts commitc9141a1cb5
. * Revert "Rework ExtensionAnonLayoutWrapperDataService to use BehaviorSubject" This reverts commit8889ed3d3c
. * simplify validateEmail method * Add primary / accent colors to wave icon * Remove debug code * PM-8111 - Tweak ShowBackButton to work * PM-8111 - LoginCompService - finish removal of setPreviousUrl from implementations. * PM-8111 - (1) Remove overriden default logo in anon layout (2) Update routing modules to have proper default login logo (3) LoginComp - update toggleLoginUiState to include logic to swap the icon back and forth as user navigates. * PM-8111 - LoginComp - on UI state change from MP entry to email entry, remove subtitle (this isn't supported yet, but it will be) * PM-8111 - LoginComp - Simplify toggleLoginUiState * PM-8111 - LoginComponent - Add known device logic into UI state change handler * PM-8111 - LoginComp - (1) Refactor name of getLoginWithDevice to be more accurate as getKnownDevice (2) Refactor calls to getKnownDevice to only occur if loginViaAuthRequestSupported * PM-8111 - LoginComp - add getKnownDevice docs * PM-8111 - LoginComponent - tweak docs * PM-8111 - LoginComp - Continue() - remove toast as the validation on submit logic currently shows validation errors - toast is extra and not needed. * Add isLoginViaAuthRequestSupported for DesktopLoginComponentService * Remove validating email on init * PM-8111 - ExtensionLoginComponentService - add tests for showBackButton * PM-8111 - style tweaks * PM-8111 - Extension - Refactor Overlay position to include extension default const to avoid repetition. * PM-8111 - Desktop AppRouting Module - remove login with passkey route as it isn't supported on desktop. * PM-8111 - Desktop - add default overlay position const * PM-8111 - DesktopLoginCompSvc - tests were not actually testing super method calls + finish testing launchSsoBrowserWindow * PM-8111 - Desktop Main.ts - remove dev test code * PM-8111 - WebLoginCompSvcTests - add success test cases for getOrgPolicies * PM-8111 - Remove duplicate translation keys * PM-8111 - DefaultLoginComponentSvcTests - add missing test * PM-8111 - DefaultLoginComponentServiceTests - add describes * PM-8111 - LoginSecondaryContentComponent - Add missing bitLink * Update to test both browser and desktop * Remove registration form test * Remove aliasing CryptoFunctionService and PlatformUtilsService as abstractions * Remove aliasing PlatformUtilsService and CryptoFunctionService as abstractions --------- Co-authored-by: Alec Rippberger <alec@livefront.com> Co-authored-by: Jared Snider <jsnider@bitwarden.com> Co-authored-by: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
parent
0254550b07
commit
df8f234b9e
@ -19,6 +19,18 @@
|
||||
"createAccount": {
|
||||
"message": "Create account"
|
||||
},
|
||||
"newToBitwarden": {
|
||||
"message": "New to Bitwarden?"
|
||||
},
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Use single sign-on"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Welcome back"
|
||||
},
|
||||
"setAStrongPassword": {
|
||||
"message": "Set a strong password"
|
||||
},
|
||||
@ -833,6 +845,9 @@
|
||||
"logIn": {
|
||||
"message": "Log in"
|
||||
},
|
||||
"logInToBitwarden": {
|
||||
"message": "Log in to Bitwarden"
|
||||
},
|
||||
"restartRegistration": {
|
||||
"message": "Restart registration"
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
@ -29,9 +29,9 @@ import { flagEnabled } from "../../platform/flags";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login.component.html",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
|
||||
showPasswordless = false;
|
||||
constructor(
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
@ -0,0 +1,85 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { flagEnabled } from "../../../platform/flags";
|
||||
import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
|
||||
import { ExtensionLoginComponentService } from "./extension-login-component.service";
|
||||
|
||||
jest.mock("../../../platform/flags", () => ({
|
||||
flagEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("ExtensionLoginComponentService", () => {
|
||||
let service: ExtensionLoginComponentService;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<BrowserPlatformUtilsService>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let extensionAnonLayoutWrapperDataService: MockProxy<ExtensionAnonLayoutWrapperDataService>;
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
platformUtilsService = mock<BrowserPlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
extensionAnonLayoutWrapperDataService = mock<ExtensionAnonLayoutWrapperDataService>();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: ExtensionLoginComponentService,
|
||||
useFactory: () =>
|
||||
new ExtensionLoginComponentService(
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
extensionAnonLayoutWrapperDataService,
|
||||
),
|
||||
},
|
||||
{ provide: DefaultLoginComponentService, useExisting: ExtensionLoginComponentService },
|
||||
{ provide: CryptoFunctionService, useValue: cryptoFunctionService },
|
||||
{ provide: EnvironmentService, useValue: environmentService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(ExtensionLoginComponentService);
|
||||
});
|
||||
|
||||
it("creates the service", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("isLoginViaAuthRequestSupported", () => {
|
||||
it("returns true if showPasswordless flag is enabled", () => {
|
||||
(flagEnabled as jest.Mock).mockReturnValue(true);
|
||||
expect(service.isLoginViaAuthRequestSupported()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if showPasswordless flag is disabled", () => {
|
||||
(flagEnabled as jest.Mock).mockReturnValue(false);
|
||||
expect(service.isLoginViaAuthRequestSupported()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showBackButton", () => {
|
||||
it("sets showBackButton in extensionAnonLayoutWrapperDataService", () => {
|
||||
service.showBackButton(true);
|
||||
expect(extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData).toHaveBeenCalledWith({
|
||||
showBackButton: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { flagEnabled } from "../../../platform/flags";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
|
||||
@Injectable()
|
||||
export class ExtensionLoginComponentService
|
||||
extends DefaultLoginComponentService
|
||||
implements LoginComponentService
|
||||
{
|
||||
constructor(
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private extensionAnonLayoutWrapperDataService: ExtensionAnonLayoutWrapperDataService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
);
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
|
||||
isLoginViaAuthRequestSupported(): boolean {
|
||||
return flagEnabled("showPasswordless");
|
||||
}
|
||||
|
||||
showBackButton(showBackButton: boolean): void {
|
||||
this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData({ showBackButton });
|
||||
}
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
import { Injectable, NgModule } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import {
|
||||
EnvironmentSelectorComponent,
|
||||
EnvironmentSelectorRouteData,
|
||||
ExtensionDefaultOverlayPosition,
|
||||
} from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect";
|
||||
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
|
||||
import {
|
||||
authGuard,
|
||||
@ -16,6 +21,8 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockIcon,
|
||||
LockV2Component,
|
||||
PasswordHintComponent,
|
||||
@ -27,6 +34,7 @@ import {
|
||||
RegistrationUserAddIcon,
|
||||
SetPasswordJitComponent,
|
||||
UserLockIcon,
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@ -42,8 +50,8 @@ import { HintComponent } from "../auth/popup/hint.component";
|
||||
import { HomeComponent } from "../auth/popup/home.component";
|
||||
import { LockComponent } from "../auth/popup/lock.component";
|
||||
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||
import { LoginViaAuthRequestComponent } from "../auth/popup/login-via-auth-request.component";
|
||||
import { LoginComponent } from "../auth/popup/login.component";
|
||||
import { RegisterComponent } from "../auth/popup/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
@ -155,7 +163,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "home",
|
||||
component: HomeComponent,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")],
|
||||
data: { state: "home" } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(Fido2V1Component, Fido2Component, {
|
||||
@ -163,12 +171,6 @@ const routes: Routes = [
|
||||
canActivate: [fido2AuthGuard],
|
||||
data: { state: "fido2" } satisfies RouteDataProperties,
|
||||
}),
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponent,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: { state: "login" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
component: LoginViaAuthRequestComponent,
|
||||
@ -440,6 +442,47 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginComponentV1,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: { state: "login" },
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
state: "login",
|
||||
showAcctSwitcher: true,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -25,8 +25,8 @@ import { HintComponent } from "../auth/popup/hint.component";
|
||||
import { HomeComponent } from "../auth/popup/home.component";
|
||||
import { LockComponent } from "../auth/popup/lock.component";
|
||||
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
|
||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||
import { LoginViaAuthRequestComponent } from "../auth/popup/login-via-auth-request.component";
|
||||
import { LoginComponent } from "../auth/popup/login.component";
|
||||
import { RegisterComponent } from "../auth/popup/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
@ -159,7 +159,7 @@ import "../platform/popup/locales";
|
||||
HintComponent,
|
||||
HomeComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
NotificationsSettingsV1Component,
|
||||
|
@ -18,17 +18,25 @@ import {
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular";
|
||||
import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
LoginComponentService,
|
||||
LockComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
AccountService,
|
||||
AccountService as AccountServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
@ -90,11 +98,13 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
|
||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||
import AutofillService from "../../autofill/services/autofill.service";
|
||||
import MainBackground from "../../background/main.background";
|
||||
@ -573,9 +583,21 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AnonLayoutWrapperDataService,
|
||||
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||
useExisting: ExtensionAnonLayoutWrapperDataService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginComponentService,
|
||||
useClass: ExtensionLoginComponentService,
|
||||
deps: [
|
||||
CryptoFunctionService,
|
||||
EnvironmentService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PlatformUtilsService,
|
||||
SsoLoginServiceAbstraction,
|
||||
ExtensionAnonLayoutWrapperDataService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockService,
|
||||
useClass: ForegroundLockService,
|
||||
@ -586,6 +608,16 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: flagEnabled("sdk") ? BrowserSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginEmailService,
|
||||
useClass: LoginEmailService,
|
||||
deps: [AccountService, AuthService, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ExtensionAnonLayoutWrapperDataService,
|
||||
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import {
|
||||
DesktopDefaultOverlayPosition,
|
||||
EnvironmentSelectorComponent,
|
||||
} from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
|
||||
import {
|
||||
authGuard,
|
||||
@ -15,6 +18,8 @@ import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-ref
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockIcon,
|
||||
LockV2Component,
|
||||
PasswordHintComponent,
|
||||
@ -26,6 +31,7 @@ import {
|
||||
RegistrationUserAddIcon,
|
||||
SetPasswordJitComponent,
|
||||
UserLockIcon,
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@ -35,8 +41,8 @@ import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { LockComponent } from "../auth/lock.component";
|
||||
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
||||
import { LoginComponentV1 } from "../auth/login/login-v1.component";
|
||||
import { LoginViaAuthRequestComponent } from "../auth/login/login-via-auth-request.component";
|
||||
import { LoginComponent } from "../auth/login/login.component";
|
||||
import { RegisterComponent } from "../auth/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
@ -69,11 +75,6 @@ const routes: Routes = [
|
||||
canActivate: [lockGuard()],
|
||||
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponent,
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
component: LoginViaAuthRequestComponent,
|
||||
@ -163,6 +164,42 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponentV1,
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
|
@ -19,28 +19,41 @@ import {
|
||||
CLIENT_TYPE,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { LockComponentService, SetPasswordJitService } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
LoginComponentService,
|
||||
SetPasswordJitService,
|
||||
LockComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginEmailService,
|
||||
PinServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import {
|
||||
AccountService,
|
||||
AccountService as AccountServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
AuthService,
|
||||
AuthService as AuthServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import {
|
||||
KdfConfigService,
|
||||
KdfConfigService as KdfConfigServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { ProcessReloadService } from "@bitwarden/common/key-management/services/process-reload.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
@ -68,7 +81,7 @@ import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KeyService,
|
||||
@ -77,6 +90,7 @@ import {
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
@ -315,11 +329,29 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginComponentService,
|
||||
useClass: DesktopLoginComponentService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EnvironmentService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
SsoLoginServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
ToastService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkClientFactory,
|
||||
useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginEmailService,
|
||||
useClass: LoginEmailService,
|
||||
deps: [AccountService, AuthService, StateProvider],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -0,0 +1,162 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { ElectronPlatformUtilsService } from "../../platform/services/electron-platform-utils.service";
|
||||
|
||||
import { DesktopLoginComponentService } from "./desktop-login-component.service";
|
||||
|
||||
const defaultIpc = {
|
||||
platform: {
|
||||
isAppImage: false,
|
||||
isSnapStore: false,
|
||||
isDev: false,
|
||||
localhostCallbackService: {
|
||||
openSsoPrompt: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(global as any).ipc = defaultIpc;
|
||||
|
||||
describe("DesktopLoginComponentService", () => {
|
||||
let service: DesktopLoginComponentService;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<ElectronPlatformUtilsService>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
let superLaunchSsoBrowserWindowSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
environmentService.environment$ = of({
|
||||
getWebVaultUrl: () => "https://webvault.bitwarden.com",
|
||||
getRegion: () => "US",
|
||||
getUrls: () => ({}),
|
||||
isCloud: () => true,
|
||||
getApiUrl: () => "https://api.bitwarden.com",
|
||||
} as Environment);
|
||||
|
||||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
platformUtilsService = mock<ElectronPlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
i18nService = mock<I18nService>();
|
||||
toastService = mock<ToastService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: DesktopLoginComponentService,
|
||||
useFactory: () =>
|
||||
new DesktopLoginComponentService(
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
i18nService,
|
||||
toastService,
|
||||
),
|
||||
},
|
||||
{ provide: DefaultLoginComponentService, useExisting: DesktopLoginComponentService },
|
||||
{ provide: CryptoFunctionService, useValue: cryptoFunctionService },
|
||||
{ provide: EnvironmentService, useValue: environmentService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DesktopLoginComponentService);
|
||||
|
||||
superLaunchSsoBrowserWindowSpy = jest.spyOn(
|
||||
DefaultLoginComponentService.prototype,
|
||||
"launchSsoBrowserWindow",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore the original ipc object after each test
|
||||
(global as any).ipc = { ...defaultIpc };
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("creates the service", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("launchSsoBrowserWindow", () => {
|
||||
// Array of all permutations of isAppImage, isSnapStore, and isDev
|
||||
const permutations = [
|
||||
[true, false, false], // Case 1: isAppImage true
|
||||
[false, true, false], // Case 2: isSnapStore true
|
||||
[false, false, true], // Case 3: isDev true
|
||||
[true, true, false], // Case 4: isAppImage and isSnapStore true
|
||||
[true, false, true], // Case 5: isAppImage and isDev true
|
||||
[false, true, true], // Case 6: isSnapStore and isDev true
|
||||
[true, true, true], // Case 7: all true
|
||||
[false, false, false], // Case 8: all false
|
||||
];
|
||||
|
||||
permutations.forEach(([isAppImage, isSnapStore, isDev]) => {
|
||||
it(`executes correct logic for isAppImage=${isAppImage}, isSnapStore=${isSnapStore}, isDev=${isDev}`, async () => {
|
||||
(global as any).ipc.platform.isAppImage = isAppImage;
|
||||
(global as any).ipc.platform.isSnapStore = isSnapStore;
|
||||
(global as any).ipc.platform.isDev = isDev;
|
||||
|
||||
const email = "user@example.com";
|
||||
const clientId = "desktop";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const state = "testState";
|
||||
const codeVerifierHash = new Uint8Array(64);
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
cryptoFunctionService.hash.mockResolvedValueOnce(codeVerifierHash);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.launchSsoBrowserWindow(email, clientId);
|
||||
|
||||
if (isAppImage || isSnapStore || isDev) {
|
||||
expect(superLaunchSsoBrowserWindowSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Assert that the standard logic is executed
|
||||
expect(ssoLoginService.setSsoEmail).toHaveBeenCalledWith(email);
|
||||
expect(passwordGenerationService.generatePassword).toHaveBeenCalledTimes(2);
|
||||
expect(cryptoFunctionService.hash).toHaveBeenCalledWith(codeVerifier, "sha256");
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
|
||||
codeChallenge,
|
||||
state,
|
||||
);
|
||||
} else {
|
||||
// If all values are false, expect the super method to be called
|
||||
expect(superLaunchSsoBrowserWindowSpy).toHaveBeenCalledWith(email, clientId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,78 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Injectable()
|
||||
export class DesktopLoginComponentService
|
||||
extends DefaultLoginComponentService
|
||||
implements LoginComponentService
|
||||
{
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected i18nService: I18nService,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
);
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
|
||||
override async launchSsoBrowserWindow(email: string, clientId: "desktop"): Promise<void | null> {
|
||||
if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) {
|
||||
return super.launchSsoBrowserWindow(email, clientId);
|
||||
}
|
||||
|
||||
// Save email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(email);
|
||||
|
||||
// Generate SSO params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
// Save SSO params
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state);
|
||||
} catch (err) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
message: this.i18nService.t("ssoError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isLoginViaAuthRequestSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
@ -34,9 +34,9 @@ const BroadcasterSubscriptionId = "LoginComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login.component.html",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
export class LoginComponent extends BaseLoginComponent implements OnInit, OnDestroy {
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||
environmentModal: ViewContainerRef;
|
||||
|
@ -6,17 +6,17 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
|
||||
import { SharedModule } from "../../app/shared/shared.module";
|
||||
|
||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
||||
import { LoginComponentV1 } from "./login-v1.component";
|
||||
import { LoginViaAuthRequestComponent } from "./login-via-auth-request.component";
|
||||
import { LoginComponent } from "./login.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, RouterModule],
|
||||
declarations: [
|
||||
LoginComponent,
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponent,
|
||||
EnvironmentSelectorComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
],
|
||||
exports: [LoginComponent, LoginViaAuthRequestComponent],
|
||||
exports: [LoginComponentV1, LoginViaAuthRequestComponent],
|
||||
})
|
||||
export class LoginModule {}
|
||||
|
@ -60,6 +60,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Welcome back"
|
||||
},
|
||||
"moveToOrgDesc": {
|
||||
"message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved."
|
||||
},
|
||||
@ -554,6 +557,9 @@
|
||||
"createAccount": {
|
||||
"message": "Create account"
|
||||
},
|
||||
"newToBitwarden": {
|
||||
"message": "New to Bitwarden?"
|
||||
},
|
||||
"setAStrongPassword": {
|
||||
"message": "Set a strong password"
|
||||
},
|
||||
@ -563,6 +569,18 @@
|
||||
"logIn": {
|
||||
"message": "Log in"
|
||||
},
|
||||
"logInToBitwarden": {
|
||||
"message": "Log in to Bitwarden"
|
||||
},
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
},
|
||||
"loginWithDevice": {
|
||||
"message": "Log in with device"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Use single sign-on"
|
||||
},
|
||||
"submit": {
|
||||
"message": "Submit"
|
||||
},
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from "./login";
|
||||
export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
|
1
apps/web/src/app/auth/core/services/login/index.ts
Normal file
1
apps/web/src/app/auth/core/services/login/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./web-login-component.service";
|
@ -0,0 +1,155 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { RouterService } from "../../../../../../../../apps/web/src/app/core";
|
||||
import { flagEnabled } from "../../../../../utils/flags";
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
import { WebLoginComponentService } from "./web-login-component.service";
|
||||
|
||||
jest.mock("../../../../../utils/flags", () => ({
|
||||
flagEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("WebLoginComponentService", () => {
|
||||
let service: WebLoginComponentService;
|
||||
let acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let internalPolicyService: MockProxy<InternalPolicyService>;
|
||||
let routerService: MockProxy<RouterService>;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
|
||||
logService = mock<LogService>();
|
||||
policyApiService = mock<PolicyApiServiceAbstraction>();
|
||||
internalPolicyService = mock<InternalPolicyService>();
|
||||
routerService = mock<RouterService>();
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WebLoginComponentService,
|
||||
{ provide: DefaultLoginComponentService, useClass: WebLoginComponentService },
|
||||
{ provide: AcceptOrganizationInviteService, useValue: acceptOrganizationInviteService },
|
||||
{ provide: LogService, useValue: logService },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: policyApiService },
|
||||
{ provide: InternalPolicyService, useValue: internalPolicyService },
|
||||
{ provide: RouterService, useValue: routerService },
|
||||
{ provide: CryptoFunctionService, useValue: cryptoFunctionService },
|
||||
{ provide: EnvironmentService, useValue: environmentService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(WebLoginComponentService);
|
||||
});
|
||||
|
||||
it("creates the service", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("isLoginViaAuthRequestSupported", () => {
|
||||
it("returns true if showPasswordless flag is enabled", () => {
|
||||
(flagEnabled as jest.Mock).mockReturnValue(true);
|
||||
expect(service.isLoginViaAuthRequestSupported()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if showPasswordless flag is disabled", () => {
|
||||
(flagEnabled as jest.Mock).mockReturnValue(false);
|
||||
expect(service.isLoginViaAuthRequestSupported()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrgPolicies", () => {
|
||||
it("returns undefined if organization invite is null", async () => {
|
||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
const result = await service.getOrgPolicies();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("logs an error if getPoliciesByToken throws an error", async () => {
|
||||
const error = new Error("Test error");
|
||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue({
|
||||
organizationId: "org-id",
|
||||
token: "token",
|
||||
email: "email",
|
||||
organizationUserId: "org-user-id",
|
||||
initOrganization: false,
|
||||
orgSsoIdentifier: "sso-id",
|
||||
orgUserHasExistingUser: false,
|
||||
organizationName: "org-name",
|
||||
});
|
||||
policyApiService.getPoliciesByToken.mockRejectedValue(error);
|
||||
await service.getOrgPolicies();
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, false], // autoEnrollEnabled, resetPasswordPolicyEnabled
|
||||
[true, true], // autoEnrollEnabled, resetPasswordPolicyEnabled
|
||||
])(
|
||||
"returns policies successfully with autoEnrollEnabled=%s and resetPasswordPolicyEnabled=%s",
|
||||
async (autoEnrollEnabled, resetPasswordPolicyEnabled) => {
|
||||
const policies: Policy[] = [new Policy()];
|
||||
const masterPasswordPolicyOptions = new MasterPasswordPolicyOptions();
|
||||
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
|
||||
resetPasswordPolicyOptions.autoEnrollEnabled = autoEnrollEnabled;
|
||||
|
||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue({
|
||||
organizationId: "org-id",
|
||||
token: "token",
|
||||
email: "email",
|
||||
organizationUserId: "org-user-id",
|
||||
initOrganization: false,
|
||||
orgSsoIdentifier: "sso-id",
|
||||
orgUserHasExistingUser: false,
|
||||
organizationName: "org-name",
|
||||
});
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue(policies);
|
||||
|
||||
internalPolicyService.getResetPasswordPolicyOptions.mockReturnValue([
|
||||
resetPasswordPolicyOptions,
|
||||
resetPasswordPolicyEnabled,
|
||||
]);
|
||||
|
||||
internalPolicyService.masterPasswordPolicyOptions$.mockReturnValue(
|
||||
of(masterPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
const result = await service.getOrgPolicies();
|
||||
|
||||
expect(result).toEqual({
|
||||
policies: policies,
|
||||
isPolicyAndAutoEnrollEnabled:
|
||||
resetPasswordPolicyEnabled && resetPasswordPolicyOptions.autoEnrollEnabled,
|
||||
enforcedPasswordPolicyOptions: masterPasswordPolicyOptions,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,94 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
DefaultLoginComponentService,
|
||||
LoginComponentService,
|
||||
PasswordPolicies,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { flagEnabled } from "../../../../../utils/flags";
|
||||
import { RouterService } from "../../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
@Injectable()
|
||||
export class WebLoginComponentService
|
||||
extends DefaultLoginComponentService
|
||||
implements LoginComponentService
|
||||
{
|
||||
constructor(
|
||||
protected acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
protected logService: LogService,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: InternalPolicyService,
|
||||
protected routerService: RouterService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
);
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
|
||||
isLoginViaAuthRequestSupported(): boolean {
|
||||
return flagEnabled("showPasswordless");
|
||||
}
|
||||
|
||||
async getOrgPolicies(): Promise<PasswordPolicies | null> {
|
||||
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||
|
||||
if (orgInvite != null) {
|
||||
let policies: Policy[];
|
||||
|
||||
try {
|
||||
policies = await this.policyApiService.getPoliciesByToken(
|
||||
orgInvite.organizationId,
|
||||
orgInvite.token,
|
||||
orgInvite.email,
|
||||
orgInvite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (policies == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
policies,
|
||||
orgInvite.organizationId,
|
||||
);
|
||||
|
||||
const isPolicyAndAutoEnrollEnabled =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
const enforcedPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(policies),
|
||||
);
|
||||
|
||||
return {
|
||||
policies,
|
||||
isPolicyAndAutoEnrollEnabled,
|
||||
enforcedPasswordPolicyOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@
|
||||
routerLink="/login-with-passkey"
|
||||
(mousedown)="$event.preventDefault()"
|
||||
>
|
||||
<span><i class="bwi bwi-passkey"></i> {{ "loginWithPasskey" | i18n }}</span>
|
||||
<span><i class="bwi bwi-passkey"></i> {{ "logInWithPasskey" | i18n }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, takeUntil } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
@ -39,14 +39,15 @@ import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login.component.html",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: Policy[];
|
||||
showPasswordless = false;
|
||||
|
||||
constructor(
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
@ -99,6 +100,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
this.showPasswordless = flagEnabled("showPasswordless");
|
||||
}
|
||||
|
||||
submitForm = async (showToast = true) => {
|
||||
return await this.submitFormHelper(showToast);
|
||||
};
|
||||
@ -106,9 +108,11 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
private async submitFormHelper(showToast: boolean) {
|
||||
await super.submit(showToast);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
// If there is an query parameter called 'org', set previousUrl to `/create-organization?org=paramValue`
|
||||
if (qParams.org != null) {
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
@ -116,13 +120,18 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
// Are they coming from an email for sponsoring a families organization
|
||||
/**
|
||||
* If there is a query parameter called 'sponsorshipToken', that means they are coming
|
||||
* from an email for sponsoring a families organization. If so, then set the prevousUrl
|
||||
* to `/setup/families-for-enterprise?token=paramValue`
|
||||
*/
|
||||
if (qParams.sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { token: qParams.sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
@ -206,10 +215,12 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
|
||||
if (this.policies == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
this.policies,
|
||||
invite.organizationId,
|
||||
);
|
||||
|
||||
// Set to true if policy enabled and auto-enroll enabled
|
||||
this.showResetPasswordAutoEnrollWarning =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
@ -5,20 +5,20 @@ import { CheckboxModule } from "@bitwarden/components";
|
||||
import { SharedModule } from "../../../app/shared";
|
||||
|
||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
|
||||
import { LoginComponentV1 } from "./login-v1.component";
|
||||
import { LoginViaAuthRequestComponent } from "./login-via-auth-request.component";
|
||||
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
||||
import { LoginComponent } from "./login.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CheckboxModule],
|
||||
declarations: [
|
||||
LoginComponent,
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
exports: [
|
||||
LoginComponent,
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginViaWebAuthnComponent,
|
||||
|
@ -27,21 +27,31 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
|
||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
LoginComponentService,
|
||||
LockComponentService,
|
||||
SetPasswordJitService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginEmailService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
InternalPolicyService,
|
||||
PolicyService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
@ -50,7 +60,7 @@ import {
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
@ -71,6 +81,7 @@ import {
|
||||
ThemeStateService,
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService as KeyServiceAbstraction, BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
@ -78,6 +89,7 @@ import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
import {
|
||||
WebSetPasswordJitService,
|
||||
WebRegistrationFinishService,
|
||||
WebLoginComponentService,
|
||||
WebLockComponentService,
|
||||
} from "../auth";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
@ -109,8 +121,8 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider(PolicyListService),
|
||||
safeProvider({
|
||||
provide: DEFAULT_VAULT_TIMEOUT,
|
||||
deps: [PlatformUtilsServiceAbstraction],
|
||||
useFactory: (platformUtilsService: PlatformUtilsServiceAbstraction): VaultTimeout =>
|
||||
deps: [PlatformUtilsService],
|
||||
useFactory: (platformUtilsService: PlatformUtilsService): VaultTimeout =>
|
||||
platformUtilsService.isDev() ? VaultTimeoutStringType.Never : 15,
|
||||
}),
|
||||
safeProvider({
|
||||
@ -148,7 +160,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PlatformUtilsServiceAbstraction,
|
||||
provide: PlatformUtilsService,
|
||||
useClass: WebPlatformUtilsService,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
@ -243,6 +255,22 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultAppIdService,
|
||||
deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginComponentService,
|
||||
useClass: WebLoginComponentService,
|
||||
deps: [
|
||||
AcceptOrganizationInviteService,
|
||||
LogService,
|
||||
PolicyApiServiceAbstraction,
|
||||
InternalPolicyService,
|
||||
RouterService,
|
||||
CryptoFunctionService,
|
||||
EnvironmentService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PlatformUtilsService,
|
||||
SsoLoginServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CollectionAdminService,
|
||||
useClass: DefaultCollectionAdminService,
|
||||
@ -253,6 +281,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginEmailService,
|
||||
useClass: LoginEmailService,
|
||||
deps: [AccountService, AuthService, StateProvider],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -22,12 +22,15 @@ import {
|
||||
RegistrationStartSecondaryComponentData,
|
||||
SetPasswordJitComponent,
|
||||
RegistrationLinkExpiredComponent,
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockV2Component,
|
||||
LockIcon,
|
||||
UserLockIcon,
|
||||
RegistrationUserAddIcon,
|
||||
RegistrationLockAltIcon,
|
||||
RegistrationExpiredLinkIcon,
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@ -43,9 +46,9 @@ import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||
import { HintComponent } from "./auth/hint.component";
|
||||
import { LockComponent } from "./auth/lock.component";
|
||||
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
|
||||
import { LoginComponentV1 } from "./auth/login/login-v1.component";
|
||||
import { LoginViaAuthRequestComponent } from "./auth/login/login-via-auth-request.component";
|
||||
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||
import { LoginComponent } from "./auth/login/login.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
|
||||
@ -180,6 +183,56 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LoginComponentV1,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logIn",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LoginComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: LoginSecondaryContentComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
|
@ -6,10 +6,11 @@ window.addEventListener("load", () => {
|
||||
const code = getQsParam("code");
|
||||
const state = getQsParam("state");
|
||||
const lastpass = getQsParam("lp");
|
||||
const clientId = getQsParam("clientId");
|
||||
|
||||
if (lastpass === "1") {
|
||||
initiateBrowserSso(code, state, true);
|
||||
} else if (state != null && state.includes(":clientId=browser")) {
|
||||
} else if (state != null && clientId == "browser") {
|
||||
initiateBrowserSso(code, state, false);
|
||||
} else {
|
||||
window.location.href = window.location.origin + "/#/sso?code=" + code + "&state=" + state;
|
||||
|
@ -939,9 +939,15 @@
|
||||
"useADifferentLogInMethod": {
|
||||
"message": "Use a different log in method"
|
||||
},
|
||||
"loginWithPasskey": {
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Use single sign-on"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Welcome back"
|
||||
},
|
||||
"invalidPasskeyPleaseTryAgain": {
|
||||
"message": "Invalid Passkey. Please try again."
|
||||
},
|
||||
@ -1023,6 +1029,9 @@
|
||||
"createAccount": {
|
||||
"message": "Create account"
|
||||
},
|
||||
"newToBitwarden": {
|
||||
"message": "New to Bitwarden?"
|
||||
},
|
||||
"setAStrongPassword": {
|
||||
"message": "Set a strong password"
|
||||
},
|
||||
@ -1038,6 +1047,9 @@
|
||||
"logIn": {
|
||||
"message": "Log in"
|
||||
},
|
||||
"logInToBitwarden": {
|
||||
"message": "Log in to Bitwarden"
|
||||
},
|
||||
"verifyIdentity": {
|
||||
"message": "Verify your Identity"
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Observable, map } from "rxjs";
|
||||
import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core";
|
||||
import { Router, ActivatedRoute } from "@angular/router";
|
||||
import { Observable, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import {
|
||||
EnvironmentService,
|
||||
@ -10,6 +10,27 @@ import {
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
export const ExtensionDefaultOverlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "top",
|
||||
overlayX: "start",
|
||||
overlayY: "bottom",
|
||||
},
|
||||
];
|
||||
export const DesktopDefaultOverlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "top",
|
||||
overlayX: "start",
|
||||
overlayY: "bottom",
|
||||
},
|
||||
];
|
||||
|
||||
export interface EnvironmentSelectorRouteData {
|
||||
overlayPosition?: ConnectedPosition[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
templateUrl: "environment-selector.component.html",
|
||||
@ -34,11 +55,9 @@ import {
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class EnvironmentSelectorComponent {
|
||||
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
||||
protected isOpen = false;
|
||||
protected ServerEnvironmentType = Region;
|
||||
protected overlayPosition: ConnectedPosition[] = [
|
||||
@Input() overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
@ -47,6 +66,8 @@ export class EnvironmentSelectorComponent {
|
||||
},
|
||||
];
|
||||
|
||||
protected isOpen = false;
|
||||
protected ServerEnvironmentType = Region;
|
||||
protected availableRegions = this.environmentService.availableRegions();
|
||||
protected selectedRegion$: Observable<RegionConfig | undefined> =
|
||||
this.environmentService.environment$.pipe(
|
||||
@ -54,11 +75,27 @@ export class EnvironmentSelectorComponent {
|
||||
map((r) => this.availableRegions.find((ar) => ar.key === r)),
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentService,
|
||||
protected router: Router,
|
||||
private route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data) => {
|
||||
if (data && data["overlayPosition"]) {
|
||||
this.overlayPosition = data["overlayPosition"];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: Region) {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (option === null) {
|
||||
|
@ -35,15 +35,17 @@ import {
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@Directive()
|
||||
export class LoginComponent extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||
export class LoginComponentV1 extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("masterPasswordInput", { static: true }) masterPasswordInput: ElementRef;
|
||||
|
||||
showPassword = false;
|
||||
formPromise: Promise<AuthResult>;
|
||||
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: (userId: UserId) => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
showLoginWithDevice: boolean;
|
||||
validatedEmail = false;
|
||||
paramEmailSet = false;
|
||||
@ -208,6 +210,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
// 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
|
||||
@ -292,6 +295,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
async validateEmail() {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
const emailValid = this.formGroup.get("email").valid;
|
||||
|
||||
if (emailValid) {
|
||||
this.toggleValidateEmail(true);
|
||||
await this.getLoginWithDevice(this.loggedEmail);
|
||||
@ -346,8 +350,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required
|
||||
// but only performed on web
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required but only performed on web
|
||||
protected async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
@ -0,0 +1,55 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { unauthUiRefreshRedirect } from "./unauth-ui-refresh-redirect";
|
||||
|
||||
describe("unauthUiRefreshRedirect", () => {
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = mock<ConfigService>();
|
||||
router = mock<Router>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true when UnauthenticatedExtensionUIRefresh flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const result = await TestBed.runInInjectionContext(() =>
|
||||
unauthUiRefreshRedirect("/redirect")(),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled", async () => {
|
||||
const mockUrlTree = mock<UrlTree>();
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
router.parseUrl.mockReturnValue(mockUrlTree);
|
||||
|
||||
const result = await TestBed.runInInjectionContext(() =>
|
||||
unauthUiRefreshRedirect("/redirect")(),
|
||||
);
|
||||
|
||||
expect(result).toBe(mockUrlTree);
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.parseUrl).toHaveBeenCalledWith("/redirect");
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { UrlTree, Router } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
/**
|
||||
* Helper function to redirect to a new URL based on the UnauthenticatedExtensionUIRefresh feature flag.
|
||||
* @param redirectUrl - The URL to redirect to if the UnauthenticatedExtensionUIRefresh flag is enabled.
|
||||
*/
|
||||
export function unauthUiRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
|
||||
return async () => {
|
||||
const configService = inject(ConfigService);
|
||||
const router = inject(Router);
|
||||
const shouldRedirect = await configService.getFeatureFlag(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
if (shouldRedirect) {
|
||||
return router.parseUrl(redirectUrl);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
@ -14,6 +14,8 @@ import {
|
||||
DefaultRegistrationFinishService,
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
LoginComponentService,
|
||||
DefaultLoginComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
@ -1334,6 +1336,17 @@ const safeProviders: SafeProvider[] = [
|
||||
useExisting: NoopViewCacheService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginComponentService,
|
||||
useClass: DefaultLoginComponentService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EnvironmentService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
SsoLoginServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkService,
|
||||
useClass: DefaultSdkService,
|
||||
|
@ -1,8 +1,11 @@
|
||||
export * from "./bitwarden-logo.icon";
|
||||
export * from "./bitwarden-shield.icon";
|
||||
export * from "./lock.icon";
|
||||
export * from "./registration-check-email.icon";
|
||||
export * from "./user-lock.icon";
|
||||
export * from "./user-verification-biometrics-fingerprint.icon";
|
||||
export * from "./wave.icon";
|
||||
export * from "./vault.icon";
|
||||
export * from "./registration-user-add.icon";
|
||||
export * from "./registration-lock-alt.icon";
|
||||
export * from "./registration-expired-link.icon";
|
||||
|
23
libs/auth/src/angular/icons/vault.icon.ts
Normal file
23
libs/auth/src/angular/icons/vault.icon.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const VaultIcon = svgIcon`
|
||||
<svg viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5.61279" y="1.5" width="108.775" height="89.1503" rx="9.25741" class="tw-stroke-art-primary" stroke-width="2"/>
|
||||
<path d="M49.5854 61.4941L49.5854 70.4652" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M49.5854 21.6851L49.5854 30.6562" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M38.6827 56.978L32.3392 63.3215" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M66.8324 28.8286L60.4889 35.1721" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M38.6827 35.1721L32.3392 28.8286" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M66.8324 63.3215L60.4889 56.978" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M74.1788 46.0513L65.2077 46.0513" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M33.6777 46.0513L24.7066 46.0514" class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round"/>
|
||||
<ellipse cx="49.5855" cy="46.0513" rx="15.0433" ry="15.0433" class="tw-stroke-art-primary" stroke-width="2"/>
|
||||
<ellipse cx="49.5855" cy="46.0513" rx="10.4146" ry="10.4146" class="tw-stroke-art-accent" />
|
||||
<path d="M14.0227 90.6504V95.0286C14.0227 96.9458 15.577 98.5001 17.4942 98.5001H27.9327C29.8499 98.5001 31.4042 96.9458 31.4042 95.0286V90.6504" class="tw-stroke-art-primary" stroke-width="2"/>
|
||||
<path d="M88.595 90.6504V95.0286C88.595 96.9458 90.1492 98.5001 92.0665 98.5001H102.505C104.422 98.5001 105.976 96.9458 105.976 95.0286V90.6504" class="tw-stroke-art-primary" stroke-width="2"/>
|
||||
<path d="M105.977 32.3381L107.588 32.3381C108.866 32.3381 109.902 31.302 109.902 30.0238L109.902 17.271C109.902 15.9928 108.866 14.9566 107.588 14.9566L105.977 14.9566" class="tw-stroke-art-primary" stroke-width="2"/>
|
||||
<path d="M105.977 77.1936L107.588 77.1936C108.866 77.1936 109.902 76.1574 109.902 74.8793L109.902 62.1265C109.902 60.8483 108.866 59.8121 107.588 59.8121L105.977 59.8121" class="tw-stroke-art-primary" stroke-width="2"/>
|
||||
<rect x="14.0227" y="9.9104" width="91.9537" height="72.8902" rx="4.6287" class="tw-stroke-art-accent" />
|
||||
<path d="M101.08 61.8082V75.5593C101.08 76.8375 100.044 77.8737 98.7654 77.8737H45.8796M19.4989 28.7961V17.122C19.4989 15.8438 20.5351 14.8076 21.8133 14.8076H74.5501" class="tw-stroke-art-accent" stroke-linecap="round"/>
|
||||
</svg>
|
||||
`;
|
34
libs/auth/src/angular/icons/wave.icon.ts
Normal file
34
libs/auth/src/angular/icons/wave.icon.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const WaveIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 120 100">
|
||||
<path
|
||||
class="tw-stroke-art-primary"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M48.603 49.08c.188-.341.365-.688.566-1.022 4.409-7.312 8.826-14.62 13.228-21.936 2.131-3.54.316-7.806-3.688-8.72-2.335-.533-4.776.562-6.204 2.844-2.924 4.673-5.823 9.362-8.732 14.043-2.2 3.542-4.384 7.094-6.609 10.62-1.844 2.926-4.231 5.315-7.246 7.035-1.851 1.057-2.841.543-3.122-1.554-.548-4.105-1.184-8.194-2.445-12.155-1.625-5.1-6.98-7.558-11.909-5.469-1.134.482-1.361.949-1.091 2.137.703 3.086 1.53 6.152 2.06 9.27.79 4.638.403 9.275-.4 13.894a90.84 90.84 0 0 0-1.352 14.404c-.062 4.798 1.476 8.947 4.824 12.337 3.655 3.702 7.422 7.313 11.933 9.989 7.128 4.23 14.348 4.162 21.605.282 3.89-2.08 7.273-4.84 10.478-7.827 8.691-8.101 5.227-5.375 16.072-11.488 4.7-2.65 9.434-5.242 14.15-7.863 2.708-1.505 3.593-4.521 2.146-7.276-1.4-2.666-4.426-3.627-7.186-2.228-6.594 3.345-13.174 6.717-19.76 10.076-.325.165-.652.323-1.085.344.371-.304.734-.619 1.116-.91 7.578-5.801 15.158-11.601 22.737-17.401 1.86-1.425 2.714-3.3 2.344-5.624-.36-2.26-1.707-3.769-3.873-4.483-1.897-.626-3.628-.245-5.257.928-7.792 5.607-15.604 11.188-23.41 16.776-.274.196-.551.389-.974.452.234-.278.454-.572.705-.835 7.24-7.632 14.493-15.25 21.72-22.893 3.354-3.549 1.622-9.313-3.127-10.382-2.274-.512-4.275.06-5.936 1.796-7.056 7.373-14.134 14.721-21.197 22.087-.293.306-.51.686-.764 1.031l-.317-.278Z"
|
||||
/>
|
||||
<path
|
||||
class="tw-stroke-art-primary"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M49.237 2.496c-13.4 0-24.262 10.863-24.262 24.262"
|
||||
/>
|
||||
<path
|
||||
class="tw-stroke-art-accent"
|
||||
stroke-linecap="round"
|
||||
d="M46.57 8.895c-8.393 0-15.196 6.804-15.196 15.197"
|
||||
/>
|
||||
<path
|
||||
class="tw-stroke-art-primary"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M84.49 93.027c13.4 0 24.262-10.863 24.262-24.262"
|
||||
/>
|
||||
<path
|
||||
class="tw-stroke-art-accent"
|
||||
stroke-linecap="round"
|
||||
d="M87.157 86.628c8.393 0 15.197-6.804 15.197-15.197"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
@ -2,9 +2,6 @@
|
||||
* This barrel file should only contain Angular exports
|
||||
*/
|
||||
|
||||
// icons
|
||||
export * from "./icons";
|
||||
|
||||
// anon layout
|
||||
export * from "./anon-layout/anon-layout.component";
|
||||
export * from "./anon-layout/anon-layout-wrapper.component";
|
||||
@ -14,15 +11,33 @@ export * from "./anon-layout/default-anon-layout-wrapper-data.service";
|
||||
// fingerprint dialog
|
||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||
|
||||
// icons
|
||||
export * from "./icons";
|
||||
|
||||
// input password
|
||||
export * from "./input-password/input-password.component";
|
||||
export * from "./input-password/password-input-result";
|
||||
|
||||
// login
|
||||
export * from "./login/login.component";
|
||||
export * from "./login/login-secondary-content.component";
|
||||
export * from "./login/login-component.service";
|
||||
export * from "./login/default-login-component.service";
|
||||
|
||||
// password callout
|
||||
export * from "./password-callout/password-callout.component";
|
||||
|
||||
// password hint
|
||||
export * from "./password-hint/password-hint.component";
|
||||
|
||||
// input password
|
||||
export * from "./input-password/input-password.component";
|
||||
export * from "./input-password/password-input-result";
|
||||
// registration
|
||||
export * from "./registration/registration-start/registration-start.component";
|
||||
export * from "./registration/registration-finish/registration-finish.component";
|
||||
export * from "./registration/registration-link-expired/registration-link-expired.component";
|
||||
export * from "./registration/registration-start/registration-start-secondary.component";
|
||||
export * from "./registration/registration-env-selector/registration-env-selector.component";
|
||||
export * from "./registration/registration-finish/registration-finish.service";
|
||||
export * from "./registration/registration-finish/default-registration-finish.service";
|
||||
|
||||
// set password (JIT user)
|
||||
export * from "./set-password-jit/set-password-jit.component";
|
||||
@ -34,15 +49,6 @@ export * from "./user-verification/user-verification-dialog.component";
|
||||
export * from "./user-verification/user-verification-dialog.types";
|
||||
export * from "./user-verification/user-verification-form-input.component";
|
||||
|
||||
// registration
|
||||
export * from "./registration/registration-start/registration-start.component";
|
||||
export * from "./registration/registration-finish/registration-finish.component";
|
||||
export * from "./registration/registration-link-expired/registration-link-expired.component";
|
||||
export * from "./registration/registration-start/registration-start-secondary.component";
|
||||
export * from "./registration/registration-env-selector/registration-env-selector.component";
|
||||
export * from "./registration/registration-finish/registration-finish.service";
|
||||
export * from "./registration/registration-finish/default-registration-finish.service";
|
||||
|
||||
// lock
|
||||
export * from "./lock/lock.component";
|
||||
export * from "./lock/lock-component.service";
|
||||
|
@ -0,0 +1,124 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Environment,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { DefaultLoginComponentService } from "./default-login-component.service";
|
||||
|
||||
jest.mock("@bitwarden/common/platform/abstractions/crypto-function.service");
|
||||
jest.mock("@bitwarden/common/platform/abstractions/environment.service");
|
||||
jest.mock("@bitwarden/common/platform/abstractions/platform-utils.service");
|
||||
jest.mock("@bitwarden/common/auth/abstractions/sso-login.service.abstraction");
|
||||
jest.mock("@bitwarden/generator-legacy");
|
||||
|
||||
describe("DefaultLoginComponentService", () => {
|
||||
let service: DefaultLoginComponentService;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
|
||||
environmentService.environment$ = of({
|
||||
getWebVaultUrl: () => "https://webvault.bitwarden.com",
|
||||
getRegion: () => "US",
|
||||
getUrls: () => ({}),
|
||||
isCloud: () => true,
|
||||
getApiUrl: () => "https://api.bitwarden.com",
|
||||
} as Environment);
|
||||
|
||||
service = new DefaultLoginComponentService(
|
||||
cryptoFunctionService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
);
|
||||
});
|
||||
|
||||
it("creates without error", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("getOrgPolicies", () => {
|
||||
it("returns null", async () => {
|
||||
const result = await service.getOrgPolicies();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLoginViaAuthRequestSupported", () => {
|
||||
it("returns false by default", () => {
|
||||
expect(service.isLoginViaAuthRequestSupported()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLoginWithPasskeySupported", () => {
|
||||
it("returns true when clientType is Web", () => {
|
||||
service["clientType"] = ClientType.Web;
|
||||
expect(service.isLoginWithPasskeySupported()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when clientType is not Web", () => {
|
||||
service["clientType"] = ClientType.Desktop;
|
||||
expect(service.isLoginWithPasskeySupported()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("launchSsoBrowserWindow", () => {
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const baseUrl = "https://webvault.bitwarden.com/#/sso";
|
||||
|
||||
beforeEach(() => {
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
clientType: ClientType.Browser,
|
||||
clientId: "browser",
|
||||
expectedRedirectUri: "https://webvault.bitwarden.com/sso-connector.html",
|
||||
},
|
||||
{
|
||||
clientType: ClientType.Desktop,
|
||||
clientId: "desktop",
|
||||
expectedRedirectUri: "bitwarden://sso-callback",
|
||||
},
|
||||
])(
|
||||
"launches SSO browser window with correct URL for $clientId client",
|
||||
async ({ clientType, clientId, expectedRedirectUri }) => {
|
||||
service["clientType"] = clientType;
|
||||
|
||||
await service.launchSsoBrowserWindow(email, clientId as "browser" | "desktop");
|
||||
|
||||
const expectedUrl = `${baseUrl}?clientId=${clientId}&redirectUri=${encodeURIComponent(expectedRedirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}`;
|
||||
|
||||
expect(ssoLoginService.setSsoEmail).toHaveBeenCalledWith(email);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalledWith(expectedUrl);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,94 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LoginComponentService, PasswordPolicies } from "@bitwarden/auth/angular";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
export class DefaultLoginComponentService implements LoginComponentService {
|
||||
protected clientType: ClientType;
|
||||
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected environmentService: EnvironmentService,
|
||||
// TODO: refactor to not use deprecated service
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async getOrgPolicies(): Promise<PasswordPolicies | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
isLoginViaAuthRequestSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isLoginWithPasskeySupported(): boolean {
|
||||
return this.clientType === ClientType.Web;
|
||||
}
|
||||
|
||||
async launchSsoBrowserWindow(
|
||||
email: string,
|
||||
clientId: "browser" | "desktop",
|
||||
): Promise<void | null> {
|
||||
// Save email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(email);
|
||||
|
||||
// Generate SSO params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
// Save SSO params
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
|
||||
// Build URL
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
|
||||
const redirectUri =
|
||||
clientId === "browser"
|
||||
? webVaultUrl + "/sso-connector.html" // Browser
|
||||
: "bitwarden://sso-callback"; // Desktop
|
||||
|
||||
// Launch browser window with URL
|
||||
this.platformUtilsService.launchUri(
|
||||
webVaultUrl +
|
||||
"/#/sso?clientId=" +
|
||||
clientId +
|
||||
"&redirectUri=" +
|
||||
encodeURIComponent(redirectUri) +
|
||||
"&state=" +
|
||||
state +
|
||||
"&codeChallenge=" +
|
||||
codeChallenge +
|
||||
"&email=" +
|
||||
encodeURIComponent(email),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op implementation of showBackButton
|
||||
*/
|
||||
showBackButton(showBackButton: boolean): void {
|
||||
return;
|
||||
}
|
||||
}
|
46
libs/auth/src/angular/login/login-component.service.ts
Normal file
46
libs/auth/src/angular/login/login-component.service.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
export interface PasswordPolicies {
|
||||
policies: Policy[];
|
||||
isPolicyAndAutoEnrollEnabled: boolean;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `LoginComponentService` allows the single libs/auth `LoginComponent` to
|
||||
* delegate all client-specific functionality to client-specific service
|
||||
* implementations of `LoginComponentService`.
|
||||
*
|
||||
* The `LoginComponentService` should not be confused with the
|
||||
* `LoginStrategyService`, which is used to determine the login strategy and
|
||||
* performs the core login logic.
|
||||
*/
|
||||
export abstract class LoginComponentService {
|
||||
/**
|
||||
* Gets the organization policies if there is an organization invite.
|
||||
* - Used by: Web
|
||||
*/
|
||||
getOrgPolicies: () => Promise<PasswordPolicies | null>;
|
||||
|
||||
/**
|
||||
* Indicates whether login with device (auth request) is supported on the given client
|
||||
*/
|
||||
isLoginViaAuthRequestSupported: () => boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether login with passkey is supported on the given client
|
||||
*/
|
||||
isLoginWithPasskeySupported: () => boolean;
|
||||
|
||||
/**
|
||||
* Launches the SSO flow in a new browser window.
|
||||
* - Used by: Browser, Desktop
|
||||
*/
|
||||
launchSsoBrowserWindow: (email: string, clientId: "browser" | "desktop") => Promise<void>;
|
||||
|
||||
/**
|
||||
* Shows the back button.
|
||||
*/
|
||||
showBackButton: (showBackButton: boolean) => void;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { RegisterRouteService } from "@bitwarden/auth/common";
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, LinkModule, RouterModule],
|
||||
template: `
|
||||
<div class="tw-text-center">
|
||||
{{ "newToBitwarden" | i18n }}
|
||||
<a bitLink [routerLink]="registerRoute$ | async">{{ "createAccount" | i18n }}</a>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class LoginSecondaryContentComponent {
|
||||
registerRouteService = inject(RegisterRouteService);
|
||||
|
||||
// TODO: remove when email verification flag is removed
|
||||
protected registerRoute$ = this.registerRouteService.registerRoute$();
|
||||
}
|
144
libs/auth/src/angular/login/login.component.html
Normal file
144
libs/auth/src/angular/login/login.component.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!--
|
||||
# Table of Contents
|
||||
|
||||
This file contains a single consolidated template for all visual clients.
|
||||
|
||||
# UI States
|
||||
|
||||
The template has two UI states, defined by the `LoginUiState` enum:
|
||||
EMAIL_ENTRY: displays the email input field + continue button
|
||||
MASTER_PASSWORD_ENTRY: displays the master password input field + login button
|
||||
-->
|
||||
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<ng-container *ngIf="loginUiState === LoginUiState.EMAIL_ENTRY">
|
||||
<!-- Email Address input -->
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
type="email"
|
||||
formControlName="email"
|
||||
bitInput
|
||||
appAutofocus
|
||||
(blur)="onEmailBlur($event)"
|
||||
(keyup.enter)="continue()"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<!-- Remember Email input -->
|
||||
<bit-form-control>
|
||||
<input type="checkbox" formControlName="rememberEmail" bitCheckbox />
|
||||
<bit-label>{{ "rememberEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div class="tw-grid tw-gap-3">
|
||||
<!-- Continue button -->
|
||||
<button type="button" bitButton block buttonType="primary" (click)="continue()">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
|
||||
<div class="tw-text-center">{{ "or" | i18n }}</div>
|
||||
|
||||
<!-- Link to Login with Passkey page -->
|
||||
<ng-container *ngIf="isLoginWithPasskeySupported()">
|
||||
<a
|
||||
bitButton
|
||||
block
|
||||
linkType="primary"
|
||||
routerLink="/login-with-passkey"
|
||||
(mousedown)="$event.preventDefault()"
|
||||
>
|
||||
<i class="bwi bwi-passkey tw-mr-1"></i>
|
||||
{{ "logInWithPasskey" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<!-- Button to Login with SSO -->
|
||||
<ng-container *ngIf="clientType === ClientType.Web">
|
||||
<a
|
||||
bitButton
|
||||
block
|
||||
buttonType="secondary"
|
||||
routerLink="/sso"
|
||||
[queryParams]="formGroup.value.email ? { email: formGroup.value.email } : {}"
|
||||
(click)="saveEmailSettings()"
|
||||
>
|
||||
<i class="bwi bwi-provider tw-mr-1"></i>
|
||||
{{ "useSingleSignOn" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="clientType === ClientType.Browser || clientType === ClientType.Desktop">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
block
|
||||
buttonType="secondary"
|
||||
(click)="
|
||||
launchSsoBrowserWindow(clientType === ClientType.Browser ? 'browser' : 'desktop')
|
||||
"
|
||||
>
|
||||
<i class="bwi bwi-provider tw-mr-1"></i>
|
||||
{{ "useSingleSignOn" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY">
|
||||
<!-- Master Password input -->
|
||||
<bit-form-field class="!tw-mb-1">
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input type="password" formControlName="masterPassword" bitInput #masterPasswordInputRef />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
|
||||
<!-- Link to Password Hint page - doesn't use bit-hint so that it doesn't get hidden on input validation errors -->
|
||||
<a bitLink routerLink="/hint" (click)="goToHint()" class="tw-inline-block tw-mb-4">
|
||||
{{ "getMasterPasswordHint" | i18n }}
|
||||
</a>
|
||||
|
||||
<!-- Captcha iframe -->
|
||||
<iframe
|
||||
[ngClass]="{ 'tw-hidden': !showCaptcha() }"
|
||||
id="hcaptcha_iframe"
|
||||
height="80"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
|
||||
<div class="tw-grid tw-gap-3">
|
||||
<!-- Submit button to Login with Master Password -->
|
||||
<button type="submit" bitButton bitFormButton block buttonType="primary">
|
||||
{{ "loginWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
|
||||
<!-- Button to Login with Device -->
|
||||
<ng-container *ngIf="loginViaAuthRequestSupported && isKnownDevice">
|
||||
<div class="tw-text-center">{{ "or" | i18n }}</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
block
|
||||
buttonType="secondary"
|
||||
(click)="startAuthRequestLogin()"
|
||||
>
|
||||
<i class="bwi bwi-mobile"></i>
|
||||
{{ "loginWithDevice" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Back button -->
|
||||
<ng-container *ngIf="shouldShowBackButton()">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
block
|
||||
buttonType="secondary"
|
||||
(click)="toggleLoginUiState(LoginUiState.EMAIL_ENTRY)"
|
||||
>
|
||||
{{ "back" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
561
libs/auth/src/angular/login/login.component.ts
Normal file
561
libs/auth/src/angular/login/login.component.ts
Normal file
@ -0,0 +1,561 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, Subject, take, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
PasswordLoginCredentials,
|
||||
RegisterRouteService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { VaultIcon, WaveIcon } from "../icons";
|
||||
|
||||
import { LoginComponentService } from "./login-component.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "LoginComponent";
|
||||
|
||||
export enum LoginUiState {
|
||||
EMAIL_ENTRY = "EmailEntry",
|
||||
MASTER_PASSWORD_ENTRY = "MasterPasswordEntry",
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./login.component.html",
|
||||
imports: [
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CheckboxModule,
|
||||
CommonModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
LinkModule,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class LoginComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef;
|
||||
@Input() captchaSiteKey: string = null;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
readonly Icons = { WaveIcon, VaultIcon };
|
||||
|
||||
captcha: CaptchaIFrame;
|
||||
captchaToken: string = null;
|
||||
clientType: ClientType;
|
||||
ClientType = ClientType;
|
||||
LoginUiState = LoginUiState;
|
||||
registerRoute$ = this.registerRouteService.registerRoute$(); // TODO: remove when email verification flag is removed
|
||||
isKnownDevice = false;
|
||||
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
|
||||
|
||||
formGroup = this.formBuilder.group(
|
||||
{
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
masterPassword: [
|
||||
"",
|
||||
[Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)],
|
||||
],
|
||||
rememberEmail: [false],
|
||||
},
|
||||
{ updateOn: "submit" },
|
||||
);
|
||||
|
||||
get emailFormControl(): FormControl<string> {
|
||||
return this.formGroup.controls.email;
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginViaAuthRequestSupported is a boolean that determines if we show the Login with device button.
|
||||
* An AuthRequest is the mechanism that allows users to login to the client via a device that is already logged in.
|
||||
*/
|
||||
loginViaAuthRequestSupported = false;
|
||||
|
||||
// Web properties
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: Policy[];
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
|
||||
// Desktop properties
|
||||
deferFocus: boolean | null = null;
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
private appIdService: AppIdService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private devicesApiService: DevicesApiServiceAbstraction,
|
||||
private environmentService: EnvironmentService,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private loginComponentService: LoginComponentService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private messagingService: MessagingService,
|
||||
private ngZone: NgZone,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: InternalPolicyService,
|
||||
private registerRouteService: RegisterRouteService,
|
||||
private router: Router,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.defaultOnInit();
|
||||
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
await this.desktopOnInit();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
// TODO: refactor to not use deprecated broadcaster service.
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { email, masterPassword } = this.formGroup.value;
|
||||
|
||||
await this.setupCaptcha();
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
this.captchaToken,
|
||||
null,
|
||||
);
|
||||
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.handleAuthResult(authResult);
|
||||
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
if (this.captchaSiteKey) {
|
||||
const content = document.getElementById("content") as HTMLDivElement;
|
||||
content.setAttribute("style", "width:335px");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the result of the authentication process.
|
||||
*
|
||||
* @param authResult
|
||||
* @returns A simple `return` statement for each conditional check.
|
||||
* If you update this method, do not forget to add a `return`
|
||||
* to each if-condition block where necessary to stop code execution.
|
||||
*/
|
||||
private async handleAuthResult(authResult: AuthResult): Promise<void> {
|
||||
if (this.handleCaptchaRequired(authResult)) {
|
||||
this.captchaSiteKey = authResult.captchaSiteKey;
|
||||
this.captcha.init(authResult.captchaSiteKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (authResult.requiresEncryptionKeyMigration) {
|
||||
/* Legacy accounts used the master key to encrypt data.
|
||||
Migration is required but only performed on Web. */
|
||||
if (this.clientType === ClientType.Web) {
|
||||
await this.router.navigate(["migrate-legacy-encryption"]);
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
message: this.i18nService.t("encryptionKeyMigrationRequired"),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (authResult.requiresTwoFactor) {
|
||||
await this.router.navigate(["2fa"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
this.loginEmailService.clearValues();
|
||||
await this.router.navigate(["update-temp-password"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If none of the above cases are true, proceed with login...
|
||||
await this.evaluatePassword();
|
||||
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
if (this.clientType === ClientType.Browser) {
|
||||
await this.router.navigate(["/tabs/vault"]);
|
||||
} else {
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise<void> {
|
||||
await this.loginComponentService.launchSsoBrowserWindow(this.emailFormControl.value, clientId);
|
||||
}
|
||||
|
||||
protected async evaluatePassword(): Promise<void> {
|
||||
try {
|
||||
// If we do not have any saved policies, attempt to load them from the service
|
||||
if (this.enforcedMasterPasswordOptions == undefined) {
|
||||
this.enforcedMasterPasswordOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.requirePasswordChange()) {
|
||||
await this.router.navigate(["update-password"]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Do not prevent unlock if there is an error evaluating policies
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* If not, returns false
|
||||
*/
|
||||
private requirePasswordChange(): boolean {
|
||||
if (
|
||||
this.enforcedMasterPasswordOptions == undefined ||
|
||||
!this.enforcedMasterPasswordOptions.enforceOnLogin
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.formGroup.value.email,
|
||||
)?.score;
|
||||
|
||||
return !this.policyService.evaluateMasterPassword(
|
||||
passwordStrength,
|
||||
masterPassword,
|
||||
this.enforcedMasterPasswordOptions,
|
||||
);
|
||||
}
|
||||
|
||||
protected showCaptcha(): boolean {
|
||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
}
|
||||
|
||||
protected async startAuthRequestLogin(): Promise<void> {
|
||||
this.formGroup.get("masterPassword")?.clearValidators();
|
||||
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
||||
|
||||
if (!this.formGroup.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
protected async validateEmail(): Promise<boolean> {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
return this.formGroup.controls.email.valid;
|
||||
}
|
||||
|
||||
protected async toggleLoginUiState(value: LoginUiState): Promise<void> {
|
||||
this.loginUiState = value;
|
||||
|
||||
if (this.loginUiState === LoginUiState.EMAIL_ENTRY) {
|
||||
this.loginComponentService.showBackButton(false);
|
||||
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: { key: "logInToBitwarden" },
|
||||
pageIcon: this.Icons.VaultIcon,
|
||||
pageSubtitle: null, // remove subtitle when going back to email entry
|
||||
});
|
||||
|
||||
// Reset master password only when going from validated to not validated so that autofill can work properly
|
||||
this.formGroup.controls.masterPassword.reset();
|
||||
|
||||
if (this.loginViaAuthRequestSupported) {
|
||||
// Reset known device state when going back to email entry if it is supported
|
||||
this.isKnownDevice = false;
|
||||
}
|
||||
} else if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
this.loginComponentService.showBackButton(true);
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageTitle: { key: "welcomeBack" },
|
||||
pageSubtitle: this.emailFormControl.value,
|
||||
pageIcon: this.Icons.WaveIcon,
|
||||
});
|
||||
|
||||
// Mark MP as untouched so that, when users enter email and hit enter, the MP field doesn't load with validation errors
|
||||
this.formGroup.controls.masterPassword.markAsUntouched();
|
||||
|
||||
// When email is validated, focus on master password after waiting for input to be rendered
|
||||
if (this.ngZone.isStable) {
|
||||
this.masterPasswordInputRef?.nativeElement?.focus();
|
||||
} else {
|
||||
this.ngZone.onStable.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.masterPasswordInputRef?.nativeElement?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.loginViaAuthRequestSupported) {
|
||||
await this.getKnownDevice(this.emailFormControl.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the email value from the input field.
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
onEmailBlur(event: Event) {
|
||||
const emailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.email.setValue(emailInput.value);
|
||||
// Call setLoginEmail so that the email is pre-populated when navigating to the "enter password" screen.
|
||||
this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
||||
}
|
||||
|
||||
isLoginWithPasskeySupported() {
|
||||
return this.loginComponentService.isLoginWithPasskeySupported();
|
||||
}
|
||||
|
||||
protected async goToHint(): Promise<void> {
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
protected async goToRegister(): Promise<void> {
|
||||
// TODO: remove when email verification flag is removed
|
||||
const registerRoute = await firstValueFrom(this.registerRoute$);
|
||||
|
||||
if (this.emailFormControl.valid) {
|
||||
await this.router.navigate([registerRoute], {
|
||||
queryParams: { email: this.emailFormControl.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate([registerRoute]);
|
||||
}
|
||||
|
||||
protected async saveEmailSettings(): Promise<void> {
|
||||
await this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
protected async continue(): Promise<void> {
|
||||
if (await this.validateEmail()) {
|
||||
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to check if the device is known.
|
||||
* Known means that the user has logged in with this device before.
|
||||
* @param email - The user's email
|
||||
*/
|
||||
private async getKnownDevice(email: string): Promise<void> {
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier);
|
||||
} catch (e) {
|
||||
this.isKnownDevice = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async setupCaptcha(): Promise<void> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
|
||||
this.captcha = new CaptchaIFrame(
|
||||
window,
|
||||
webVaultUrl,
|
||||
this.i18nService,
|
||||
(token: string) => {
|
||||
this.captchaToken = token;
|
||||
},
|
||||
(error: string) => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: error,
|
||||
});
|
||||
},
|
||||
(info: string) => {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: this.i18nService.t("info"),
|
||||
message: info,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private handleCaptchaRequired(authResult: AuthResult): boolean {
|
||||
return !Utils.isNullOrWhitespace(authResult.captchaSiteKey);
|
||||
}
|
||||
|
||||
private async loadEmailSettings(): Promise<void> {
|
||||
// Try to load the email from memory first
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
if (email) {
|
||||
this.formGroup.controls.email.setValue(email);
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||
} else {
|
||||
// If there is no email in memory, check for a storedEmail on disk
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
|
||||
if (storedEmail) {
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
// If there is a storedEmail, rememberEmail defaults to true
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private focusInput() {
|
||||
document
|
||||
.getElementById(
|
||||
this.emailFormControl.value == null || this.emailFormControl.value === ""
|
||||
? "email"
|
||||
: "masterPassword",
|
||||
)
|
||||
?.focus();
|
||||
}
|
||||
|
||||
private async defaultOnInit(): Promise<void> {
|
||||
// If there's an existing org invite, use it to get the password policies
|
||||
const orgPolicies = await this.loginComponentService.getOrgPolicies();
|
||||
|
||||
this.policies = orgPolicies?.policies;
|
||||
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled;
|
||||
|
||||
let paramEmailIsSet = false;
|
||||
|
||||
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (params) {
|
||||
const qParamsEmail = params.email;
|
||||
|
||||
// If there is an email in the query params, set that email as the form field value
|
||||
if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(qParamsEmail);
|
||||
paramEmailIsSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no params or no email in the query params, loadEmailSettings from state
|
||||
if (!paramEmailIsSet) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
|
||||
if (this.loginViaAuthRequestSupported) {
|
||||
await this.getKnownDevice(this.emailFormControl.value);
|
||||
}
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.activatedRoute) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private async desktopOnInit(): Promise<void> {
|
||||
// TODO: refactor to not use deprecated broadcaster service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if the back button should be shown.
|
||||
* @returns true if the back button should be shown.
|
||||
*/
|
||||
protected shouldShowBackButton(): boolean {
|
||||
return (
|
||||
this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY &&
|
||||
this.clientType !== ClientType.Browser
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import {
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { LoginEmailService } from "../../../common";
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { RegistrationUserAddIcon } from "../../icons";
|
||||
import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon";
|
||||
@ -89,6 +90,7 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private accountApiService: AccountApiService,
|
||||
private router: Router,
|
||||
private loginEmailService: LoginEmailService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
) {
|
||||
this.isSelfHost = platformUtilsService.isSelfHost();
|
||||
@ -99,6 +101,15 @@ export class RegistrationStartComponent implements OnInit, OnDestroy {
|
||||
this.registrationStartStateChange.emit(this.state);
|
||||
|
||||
this.listenForQueryParamChanges();
|
||||
|
||||
/**
|
||||
* If the user has a login email, set the email field to the login email.
|
||||
*/
|
||||
this.loginEmailService.loginEmail$.pipe(takeUntil(this.destroy$)).subscribe((email) => {
|
||||
if (email) {
|
||||
this.formGroup.patchValue({ email });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private listenForQueryParamChanges() {
|
||||
|
@ -4,7 +4,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
import { of, BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
@ -30,6 +30,7 @@ import {
|
||||
// FIXME: remove `/apps` import from `/libs`
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests";
|
||||
import { LoginEmailService } from "../../../common";
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { AnonLayoutWrapperData } from "../../anon-layout/anon-layout-wrapper.component";
|
||||
|
||||
@ -45,6 +46,7 @@ const decorators = (options: {
|
||||
queryParams?: Params;
|
||||
clientType?: ClientType;
|
||||
defaultRegion?: Region;
|
||||
initialLoginEmail?: string;
|
||||
}) => {
|
||||
return [
|
||||
moduleMetadata({
|
||||
@ -90,6 +92,12 @@ const decorators = (options: {
|
||||
getClientType: () => options.clientType || ClientType.Web,
|
||||
} as Partial<PlatformUtilsService>,
|
||||
},
|
||||
{
|
||||
provide: LoginEmailService,
|
||||
useValue: {
|
||||
loginEmail$: new BehaviorSubject<string | null>(options.initialLoginEmail || null),
|
||||
} as Partial<LoginEmailService>,
|
||||
},
|
||||
{
|
||||
provide: AnonLayoutWrapperDataService,
|
||||
useValue: {
|
||||
@ -159,6 +167,21 @@ export const WebUSRegionQueryParamsExample: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const WebUSRegionWithInitialLoginEmailExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-registration-start></auth-registration-start>
|
||||
`,
|
||||
}),
|
||||
decorators: decorators({
|
||||
clientType: ClientType.Web,
|
||||
queryParams: {},
|
||||
defaultRegion: Region.US,
|
||||
initialLoginEmail: "example@bitwarden.com",
|
||||
}),
|
||||
};
|
||||
|
||||
export const DesktopUSRegionExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
|
Loading…
Reference in New Issue
Block a user