1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

feat(redirectToVaultIfUnlockedGuard): [Auth/PM-20623] RedirectToVaultIfUnlocked Guard (#15236)

Adds a `redirect-to-vault-if-unlocked.guard.ts` that does the following:
- If there is no active user, allow access to the route
- If the user is specifically Unlocked, redirect the user to /vault
- Otherwise, allow access to the route (fallback/default)
This commit is contained in:
rr-bw
2025-07-17 14:24:53 -07:00
committed by GitHub
parent b4120e0e3f
commit 9ca265c543
6 changed files with 196 additions and 16 deletions

View File

@@ -4,3 +4,4 @@ export * from "./lock.guard";
export * from "./redirect/redirect.guard";
export * from "./tde-decryption-required.guard";
export * from "./unauth.guard";
export * from "./redirect-to-vault-if-unlocked/redirect-to-vault-if-unlocked.guard";

View File

@@ -0,0 +1,19 @@
# RedirectToVaultIfUnlocked Guard
The `redirectToVaultIfUnlocked` redirects the user to `/vault` if they are `Unlocked`. Otherwise, it allows access to the route.
This is particularly useful for routes that can handle BOTH unauthenticated AND authenticated-but-locked users (which makes the `authGuard` unusable on those routes).
<br>
### Special Use Case - Authenticating in the Extension Popout
Imagine a user is going through the Login with Device flow in the Extension pop*out*:
- They open the pop*out* while on `/login-with-device`
- The approve the login from another device
- They are authenticated and routed to `/vault` while in the pop*out*
If the `redirectToVaultIfUnlocked` were NOT applied, if this user now opens the pop*up* they would be shown the `/login-with-device`, not their `/vault`.
But by adding the `redirectToVaultIfUnlocked` to `/login-with-device` we make sure to check if the user has already `Unlocked`, and if so, route them to `/vault` upon opening the pop*up*.

View File

@@ -0,0 +1,98 @@
import { TestBed } from "@angular/core/testing";
import { Router, provideRouter } from "@angular/router";
import { mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserId } from "@bitwarden/common/types/guid";
import { redirectToVaultIfUnlockedGuard } from "./redirect-to-vault-if-unlocked.guard";
describe("redirectToVaultIfUnlockedGuard", () => {
const activeUser: Account = {
id: "userId" as UserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
};
const setup = (activeUser: Account | null, authStatus: AuthenticationStatus | null) => {
const accountService = mock<AccountService>();
const authService = mock<AuthService>();
accountService.activeAccount$ = new BehaviorSubject<Account | null>(activeUser);
authService.authStatusFor$.mockReturnValue(of(authStatus));
const testBed = TestBed.configureTestingModule({
providers: [
{ provide: AccountService, useValue: accountService },
{ provide: AuthService, useValue: authService },
provideRouter([
{ path: "", component: EmptyComponent },
{ path: "vault", component: EmptyComponent },
{
path: "guarded-route",
component: EmptyComponent,
canActivate: [redirectToVaultIfUnlockedGuard()],
},
]),
],
});
return {
router: testBed.inject(Router),
};
};
it("should be created", () => {
const { router } = setup(null, null);
expect(router).toBeTruthy();
});
it("should redirect to /vault if the user is AuthenticationStatus.Unlocked", async () => {
// Arrange
const { router } = setup(activeUser, AuthenticationStatus.Unlocked);
// Act
await router.navigate(["guarded-route"]);
// Assert
expect(router.url).toBe("/vault");
});
it("should allow navigation to continue to the route if there is no active user", async () => {
// Arrange
const { router } = setup(null, null);
// Act
await router.navigate(["guarded-route"]);
// Assert
expect(router.url).toBe("/guarded-route");
});
it("should allow navigation to continue to the route if the user is AuthenticationStatus.LoggedOut", async () => {
// Arrange
const { router } = setup(null, AuthenticationStatus.LoggedOut);
// Act
await router.navigate(["guarded-route"]);
// Assert
expect(router.url).toBe("/guarded-route");
});
it("should allow navigation to continue to the route if the user is AuthenticationStatus.Locked", async () => {
// Arrange
const { router } = setup(null, AuthenticationStatus.Locked);
// Act
await router.navigate(["guarded-route"]);
// Assert
expect(router.url).toBe("/guarded-route");
});
});

View File

@@ -0,0 +1,36 @@
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
/**
* Redirects the user to `/vault` if they are `Unlocked`. Otherwise, it allows access to the route.
* See ./redirect-to-vault-if-unlocked/README.md for more details.
*/
export function redirectToVaultIfUnlockedGuard(): CanActivateFn {
return async () => {
const accountService = inject(AccountService);
const authService = inject(AuthService);
const router = inject(Router);
const activeUser = await firstValueFrom(accountService.activeAccount$);
// If there is no active user, allow access to the route
if (!activeUser) {
return true;
}
const authStatus = await firstValueFrom(authService.authStatusFor$(activeUser.id));
// If user is Unlocked, redirect to vault
if (authStatus === AuthenticationStatus.Unlocked) {
return router.createUrlTree(["/vault"]);
}
// If user is LoggedOut or Locked, allow access to the route
return true;
};
}