mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +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:
@@ -12,6 +12,7 @@ import {
|
||||
authGuard,
|
||||
lockGuard,
|
||||
redirectGuard,
|
||||
redirectToVaultIfUnlockedGuard,
|
||||
tdeDecryptionRequiredGuard,
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
@@ -454,6 +455,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
canActivate: [redirectToVaultIfUnlockedGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
@@ -502,6 +504,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
canActivate: [redirectToVaultIfUnlockedGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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*.
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
# Authentication Flows Documentation
|
||||
# Login via Auth Request Documentation
|
||||
|
||||
<br>
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
> - [Standard Auth Request Flows](#standard-auth-request-flows)
|
||||
> - [Admin Auth Request Flow](#admin-auth-request-flow)
|
||||
> - [Summary Table](#summary-table)
|
||||
> - [State Management](#state-management)
|
||||
|
||||
<br>
|
||||
|
||||
## Standard Auth Request Flows
|
||||
|
||||
### Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory
|
||||
|
||||
1. Unauthed user clicks "Login with device"
|
||||
2. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||
2. Navigates to `/login-with-device` which creates a `StandardAuthRequest`
|
||||
3. Receives approval from a device with authRequestPublicKey(masterKey)
|
||||
4. Decrypts masterKey
|
||||
5. Decrypts userKey
|
||||
@@ -14,7 +25,7 @@
|
||||
### Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory
|
||||
|
||||
1. Unauthed user clicks "Login with device"
|
||||
2. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||
2. Navigates to `/login-with-device` which creates a `StandardAuthRequest`
|
||||
3. Receives approval from a device with authRequestPublicKey(userKey)
|
||||
4. Decrypts userKey
|
||||
5. Proceeds to vault
|
||||
@@ -34,9 +45,9 @@ get into this flow:
|
||||
### Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory
|
||||
|
||||
1. SSO TD user authenticates via SSO
|
||||
2. Navigates to /login-initiated
|
||||
2. Navigates to `/login-initiated`
|
||||
3. Clicks "Approve from your other device"
|
||||
4. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||
4. Navigates to `/login-with-device` which creates a `StandardAuthRequest`
|
||||
5. Receives approval from device with authRequestPublicKey(masterKey)
|
||||
6. Decrypts masterKey
|
||||
7. Decrypts userKey
|
||||
@@ -46,22 +57,24 @@ get into this flow:
|
||||
### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory
|
||||
|
||||
1. SSO TD user authenticates via SSO
|
||||
2. Navigates to /login-initiated
|
||||
2. Navigates to `/login-initiated`
|
||||
3. Clicks "Approve from your other device"
|
||||
4. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||
4. Navigates to `/login-with-device` which creates a `StandardAuthRequest`
|
||||
5. Receives approval from device with authRequestPublicKey(userKey)
|
||||
6. Decrypts userKey
|
||||
7. Establishes trust (if required)
|
||||
8. Proceeds to vault
|
||||
|
||||
<br>
|
||||
|
||||
## Admin Auth Request Flow
|
||||
|
||||
### Flow: Authed SSO TD user requests admin approval
|
||||
|
||||
1. SSO TD user authenticates via SSO
|
||||
2. Navigates to /login-initiated
|
||||
2. Navigates to `/login-initiated`
|
||||
3. Clicks "Request admin approval"
|
||||
4. Navigates to /admin-approval-requested which creates an AdminAuthRequest
|
||||
4. Navigates to `/admin-approval-requested` which creates an `AdminAuthRequest`
|
||||
5. Receives approval from device with authRequestPublicKey(userKey)
|
||||
6. Decrypts userKey
|
||||
7. Establishes trust (if required)
|
||||
@@ -70,21 +83,25 @@ get into this flow:
|
||||
**Note:** TDE users are required to be enrolled in admin account recovery, which gives the admin access to the user's
|
||||
userKey. This is how admins are able to send over the authRequestPublicKey(userKey) to the user to allow them to unlock.
|
||||
|
||||
<br>
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory\* |
|
||||
| --------------- | ----------- | --------------------------------------------------- | ------------------------- | ------------------------------------------------- |
|
||||
| Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes |
|
||||
| Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no |
|
||||
| Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes |
|
||||
| Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no |
|
||||
| Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey |
|
||||
| Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory\* |
|
||||
| --------------- | ----------- | ----------------------------------------------------- | --------------------------- | ------------------------------------------------- |
|
||||
| Standard Flow 1 | unauthed | "Login with device" [`/login`] | `/login-with-device` | yes |
|
||||
| Standard Flow 2 | unauthed | "Login with device" [`/login`] | `/login-with-device` | no |
|
||||
| Standard Flow 3 | authed | "Approve from your other device" [`/login-initiated`] | `/login-with-device` | yes |
|
||||
| Standard Flow 4 | authed | "Approve from your other device" [`/login-initiated`] | `/login-with-device` | no |
|
||||
| Admin Flow | authed | "Request admin approval"<br>[`/login-initiated`] | `/admin-approval-requested` | NA - admin requests always send encrypted userKey |
|
||||
|
||||
**Note:** The phrase "in memory" here is important. It is possible for a user to have a master password for their
|
||||
account, but not have a masterKey IN MEMORY for a specific device. For example, if a user registers an account with a
|
||||
master password, then joins an SSO TD org, then logs in to a device via SSO and admin auth request, they are now logged
|
||||
into that device but that device does not have masterKey IN MEMORY.
|
||||
|
||||
<br>
|
||||
|
||||
## State Management
|
||||
|
||||
### View Cache
|
||||
@@ -102,6 +119,8 @@ The cache is used to:
|
||||
2. Allow resumption of pending auth requests
|
||||
3. Enable processing of approved requests after extension close and reopen.
|
||||
|
||||
<br>
|
||||
|
||||
### Component State Variables
|
||||
|
||||
Key state variables maintained during the authentication process:
|
||||
@@ -149,6 +168,8 @@ protected flow = Flow.StandardAuthRequest
|
||||
- Affects UI rendering and request handling
|
||||
- Set based on route and authentication state
|
||||
|
||||
<br>
|
||||
|
||||
### State Flow Examples
|
||||
|
||||
#### Standard Auth Request Cache Flow
|
||||
@@ -186,6 +207,8 @@ protected flow = Flow.StandardAuthRequest
|
||||
- Either resumes monitoring or starts new request
|
||||
- Clears state after successful approval
|
||||
|
||||
<br>
|
||||
|
||||
### State Cleanup
|
||||
|
||||
State cleanup occurs in several scenarios:
|
||||
Reference in New Issue
Block a user