mirror of
https://github.com/bitwarden/browser
synced 2026-02-22 12:24:01 +00:00
Merge remote-tracking branch 'origin' into auth/pm-18720/change-password-component-non-dialog-v2
This commit is contained in:
@@ -53,6 +53,8 @@ import { PasswordInputResult } from "./password-input-result";
|
||||
* Determines which form elements will be displayed in the UI
|
||||
* and which cryptographic keys will be created and emitted.
|
||||
*/
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum InputPasswordFlow {
|
||||
/**
|
||||
* Form elements displayed:
|
||||
|
||||
@@ -41,6 +41,8 @@ import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper
|
||||
|
||||
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum State {
|
||||
NewUser,
|
||||
ExistingUserUntrustedDevice,
|
||||
|
||||
@@ -23,12 +23,10 @@ import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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";
|
||||
@@ -42,6 +40,8 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
||||
import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service";
|
||||
import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum Flow {
|
||||
StandardAuthRequest, // when user clicks "Login with device" from /login or "Approve from your other device" from /login-initiated
|
||||
AdminAuthRequest, // when user clicks "Request admin approval" from /login-initiated
|
||||
@@ -101,7 +101,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private validationService: ValidationService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private loginViaAuthRequestCacheService: LoginViaAuthRequestCacheService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
|
||||
@@ -132,7 +131,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Get the authStatus early because we use it in both flows
|
||||
this.authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
await this.loginViaAuthRequestCacheService.init();
|
||||
|
||||
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
||||
|
||||
@@ -410,24 +408,22 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
const authRequestResponse: AuthRequestResponse =
|
||||
await this.authRequestApiService.postAuthRequest(authRequest);
|
||||
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM9112_DeviceApprovalPersistence)) {
|
||||
if (!this.authRequestKeyPair.privateKey) {
|
||||
this.logService.error("No private key when trying to cache the login view.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.accessCode) {
|
||||
this.logService.error("No access code when trying to cache the login view.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginViaAuthRequestCacheService.cacheLoginView(
|
||||
authRequestResponse.id,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
this.accessCode,
|
||||
);
|
||||
if (!this.authRequestKeyPair.privateKey) {
|
||||
this.logService.error("No private key when trying to cache the login view.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.accessCode) {
|
||||
this.logService.error("No access code when trying to cache the login view.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginViaAuthRequestCacheService.cacheLoginView(
|
||||
authRequestResponse.id,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
this.accessCode,
|
||||
);
|
||||
|
||||
if (authRequestResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ import { LoginComponentService, PasswordPolicies } from "./login-component.servi
|
||||
|
||||
const BroadcasterSubscriptionId = "LoginComponent";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum LoginUiState {
|
||||
EMAIL_ENTRY = "EmailEntry",
|
||||
MASTER_PASSWORD_ENTRY = "MasterPasswordEntry",
|
||||
|
||||
@@ -26,6 +26,8 @@ import { RegistrationUserAddIcon } from "../../icons";
|
||||
import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon";
|
||||
import { RegistrationEnvSelectorComponent } from "../registration-env-selector/registration-env-selector.component";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum RegistrationStartState {
|
||||
USER_DATA_ENTRY = "UserDataEntry",
|
||||
CHECK_EMAIL = "CheckEmail",
|
||||
|
||||
@@ -155,7 +155,14 @@ export class SsoComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect if we have landed here but only have an SSO identifier in the URL.
|
||||
// Detect if we are on the first portion of the SSO flow
|
||||
// and have been sent here from another client with the info in query params.
|
||||
// If so, we want to initialize the SSO flow with those values.
|
||||
if (this.hasParametersFromOtherClientRedirect(qParams)) {
|
||||
this.initializeFromRedirectFromOtherClient(qParams);
|
||||
}
|
||||
|
||||
// Detect if we have landed here with an SSO identifier in the URL.
|
||||
// This is used by integrations that want to "short-circuit" the login to send users
|
||||
// directly to their IdP to simulate IdP-initiated SSO, so we submit automatically.
|
||||
if (qParams.identifier != null) {
|
||||
@@ -165,13 +172,6 @@ export class SsoComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect if we are on the first portion of the SSO flow
|
||||
// and have been sent here from another client with the info in query params.
|
||||
// If so, we want to initialize the SSO flow with those values.
|
||||
if (this.hasParametersFromOtherClientRedirect(qParams)) {
|
||||
this.initializeFromRedirectFromOtherClient(qParams);
|
||||
}
|
||||
|
||||
// Try to determine the identifier using claimed domain or local state
|
||||
// persisted from the user's last login attempt.
|
||||
await this.initializeIdentifierFromEmailOrStorage();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum LegacyKeyMigrationAction {
|
||||
PREVENT_LOGIN_AND_SHOW_REQUIRE_MIGRATION_WARNING,
|
||||
NAVIGATE_TO_MIGRATION_COMPONENT,
|
||||
}
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum DuoLaunchAction {
|
||||
DIRECT_LAUNCH,
|
||||
SINGLE_ACTION_POPOUT,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum ActiveClientVerificationOption {
|
||||
MasterPassword = "masterPassword",
|
||||
Pin = "pin",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum ValidationGoal {
|
||||
InputsShouldMatch,
|
||||
InputsShouldNotMatch,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { LoginViaAuthRequestCacheService } from "./default-login-via-auth-request-cache.service";
|
||||
@@ -14,74 +13,40 @@ describe("LoginViaAuthRequestCache", () => {
|
||||
|
||||
const cacheSignal = signal<LoginViaAuthRequestView | null>(null);
|
||||
const getCacheSignal = jest.fn().mockReturnValue(cacheSignal);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const cacheSetMock = jest.spyOn(cacheSignal, "set");
|
||||
|
||||
beforeEach(() => {
|
||||
getCacheSignal.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
cacheSetMock.mockClear();
|
||||
|
||||
testBed = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ViewCacheService, useValue: { signal: getCacheSignal } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
LoginViaAuthRequestCacheService,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature enabled", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockResolvedValue(true);
|
||||
});
|
||||
it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => {
|
||||
cacheSignal.set({ ...buildMockState() });
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
|
||||
it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => {
|
||||
cacheSignal.set({ ...buildMockState() });
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
await service.init();
|
||||
|
||||
expect(service.getCachedLoginViaAuthRequestView()).toEqual({
|
||||
...buildMockState(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the signal value", async () => {
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
await service.init();
|
||||
|
||||
const parameters = buildAuthenticMockAuthView();
|
||||
|
||||
service.cacheLoginView(parameters.id, parameters.privateKey, parameters.accessCode);
|
||||
|
||||
expect(cacheSignal.set).toHaveBeenCalledWith({
|
||||
id: parameters.id,
|
||||
privateKey: Utils.fromBufferToB64(parameters.privateKey),
|
||||
accessCode: parameters.accessCode,
|
||||
});
|
||||
expect(service.getCachedLoginViaAuthRequestView()).toEqual({
|
||||
...buildMockState(),
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature disabled", () => {
|
||||
beforeEach(async () => {
|
||||
cacheSignal.set({ ...buildMockState() } as LoginViaAuthRequestView);
|
||||
getFeatureFlag.mockResolvedValue(false);
|
||||
cacheSetMock.mockClear();
|
||||
it("updates the signal value", async () => {
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
await service.init();
|
||||
});
|
||||
const parameters = buildAuthenticMockAuthView();
|
||||
|
||||
it("`getCachedCipherView` returns null", () => {
|
||||
expect(service.getCachedLoginViaAuthRequestView()).toBeNull();
|
||||
});
|
||||
service.cacheLoginView(parameters.id, parameters.privateKey, parameters.accessCode);
|
||||
|
||||
it("does not update the signal value", () => {
|
||||
const params = buildAuthenticMockAuthView();
|
||||
|
||||
service.cacheLoginView(params.id, params.privateKey, params.accessCode);
|
||||
|
||||
expect(cacheSignal.set).not.toHaveBeenCalled();
|
||||
expect(cacheSignal.set).toHaveBeenCalledWith({
|
||||
id: parameters.id,
|
||||
privateKey: Utils.fromBufferToB64(parameters.privateKey),
|
||||
accessCode: parameters.accessCode,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache";
|
||||
@@ -17,10 +15,6 @@ const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache";
|
||||
@Injectable()
|
||||
export class LoginViaAuthRequestCacheService {
|
||||
private viewCacheService: ViewCacheService = inject(ViewCacheService);
|
||||
private configService: ConfigService = inject(ConfigService);
|
||||
|
||||
/** True when the `PM9112_DeviceApproval` flag is enabled */
|
||||
private featureEnabled: boolean = false;
|
||||
|
||||
private defaultLoginViaAuthRequestCache: WritableSignal<LoginViaAuthRequestView | null> =
|
||||
this.viewCacheService.signal<LoginViaAuthRequestView | null>({
|
||||
@@ -31,23 +25,10 @@ export class LoginViaAuthRequestCacheService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Must be called once before interacting with the cached data, otherwise methods will be noop.
|
||||
*/
|
||||
async init() {
|
||||
this.featureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM9112_DeviceApprovalPersistence,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cache with the new LoginView.
|
||||
*/
|
||||
cacheLoginView(id: string, privateKey: Uint8Array, accessCode: string): void {
|
||||
if (!this.featureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When the keys get stored they should be converted to a B64 string to ensure
|
||||
// data can be properly formed when json-ified. If not done, they are not stored properly and
|
||||
// will not be parsable by the cryptography library after coming out of storage.
|
||||
@@ -59,10 +40,6 @@ export class LoginViaAuthRequestCacheService {
|
||||
}
|
||||
|
||||
clearCacheLoginView(): void {
|
||||
if (!this.featureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.defaultLoginViaAuthRequestCache.set(null);
|
||||
}
|
||||
|
||||
@@ -70,10 +47,6 @@ export class LoginViaAuthRequestCacheService {
|
||||
* Returns the cached LoginViaAuthRequestView when available.
|
||||
*/
|
||||
getCachedLoginViaAuthRequestView(): LoginViaAuthRequestView | null {
|
||||
if (!this.featureEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.defaultLoginViaAuthRequestCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,27 @@ describe("SsoUrlService", () => {
|
||||
);
|
||||
expect(result).toBe(expectedUrl);
|
||||
});
|
||||
|
||||
it("should build CLI SSO URL with Org SSO Identifier correctly", () => {
|
||||
const baseUrl = "https://web-vault.bitwarden.com";
|
||||
const clientType = ClientType.Cli;
|
||||
const redirectUri = "https://localhost:1000";
|
||||
const state = "abc123";
|
||||
const codeChallenge = "xyz789";
|
||||
const email = "test@bitwarden.com";
|
||||
const orgSsoIdentifier = "test-org";
|
||||
|
||||
const expectedUrl = `${baseUrl}/#/sso?clientId=cli&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}&identifier=${encodeURIComponent(orgSsoIdentifier)}`;
|
||||
|
||||
const result = service.buildSsoUrl(
|
||||
baseUrl,
|
||||
clientType,
|
||||
redirectUri,
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
expect(result).toBe(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export class SsoUrlService {
|
||||
* @param state A state value that will be peristed through the SSO flow
|
||||
* @param codeChallenge A challenge value that will be used to verify the SSO code after authentication
|
||||
* @param email The optional email adddress of the user initiating SSO, which will be used to look up the org SSO identifier
|
||||
* @param orgSsoIdentifier The optional SSO identifier of the org that is initiating SSO
|
||||
* @returns The URL for redirecting users to the web app SSO component
|
||||
*/
|
||||
buildSsoUrl(
|
||||
@@ -20,6 +21,7 @@ export class SsoUrlService {
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
email?: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): string {
|
||||
let url =
|
||||
webAppUrl +
|
||||
@@ -36,6 +38,10 @@ export class SsoUrlService {
|
||||
url += "&email=" + encodeURIComponent(email);
|
||||
}
|
||||
|
||||
if (orgSsoIdentifier) {
|
||||
url += "&identifier=" + encodeURIComponent(orgSsoIdentifier);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user