1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-20540] Deep-link refactor to fix SSO deep links (#14587)

* PM-20540 - TwoFactorAuthComponent - Refactor determineDefaultSuccessRoute to rely on user's auth status as the loginStrategyService's state is cleared after successful AuthN

* PM-20540 - DeepLinkGuard - Refactor to exempt login-initiated so that TDE + unlock with MP + deep link works.

* doc: Add documentation and change folder structure.
* test: add test for new excluded route.

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
This commit is contained in:
Ike
2025-05-21 08:24:17 -04:00
committed by GitHub
parent 2e4b310137
commit ae35cb4e65
11 changed files with 153 additions and 68 deletions

View File

@@ -16,8 +16,10 @@ import {
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -72,6 +74,7 @@ describe("TwoFactorAuthComponent", () => {
let mockEnvService: MockProxy<EnvironmentService>;
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
let mockTwoFactorAuthCompCacheService: MockProxy<TwoFactorAuthComponentCacheService>;
let mockAuthService: MockProxy<AuthService>;
let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions;
@@ -106,6 +109,7 @@ describe("TwoFactorAuthComponent", () => {
mockDialogService = mock<DialogService>();
mockToastService = mock<ToastService>();
mockTwoFactorAuthCompService = mock<TwoFactorAuthComponentService>();
mockAuthService = mock<AuthService>();
mockEnvService = mock<EnvironmentService>();
mockLoginSuccessHandlerService = mock<LoginSuccessHandlerService>();
@@ -204,6 +208,7 @@ describe("TwoFactorAuthComponent", () => {
provide: TwoFactorAuthComponentCacheService,
useValue: mockTwoFactorAuthCompCacheService,
},
{ provide: AuthService, useValue: mockAuthService },
],
});
@@ -295,6 +300,7 @@ describe("TwoFactorAuthComponent", () => {
it("navigates to the component's defined success route (vault is default) when the login is successful", async () => {
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
// Act
await component.submit("testToken");
@@ -316,13 +322,14 @@ describe("TwoFactorAuthComponent", () => {
async (authType, expectedRoute) => {
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
currentAuthTypeSubject.next(authType);
mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Locked);
// Act
await component.submit("testToken");
// Assert
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate).toHaveBeenCalledWith(["lock"], {
expect(mockRouter.navigate).toHaveBeenCalledWith([expectedRoute], {
queryParams: {
identifier: component.orgSsoIdentifier,
},

View File

@@ -24,9 +24,10 @@ import {
LoginSuccessHandlerService,
} from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -167,6 +168,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
private environmentService: EnvironmentService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService,
private authService: AuthService,
) {}
async ngOnInit() {
@@ -507,8 +509,8 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
}
private async determineDefaultSuccessRoute(): Promise<string> {
const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$);
if (authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey) {
const activeAccountStatus = await firstValueFrom(this.authService.activeAccountStatus$);
if (activeAccountStatus === AuthenticationStatus.Locked) {
return "lock";
}

View File

@@ -391,7 +391,7 @@ export class Utils {
return str == null || typeof str !== "string" || str.trim() === "";
}
static isNullOrEmpty(str: string): boolean {
static isNullOrEmpty(str: string | null): boolean {
return str == null || typeof str !== "string" || str == "";
}