Skip to content

Authentication

Paulo Gomes da Cruz Junior edited this page Oct 23, 2026 · 5 revisions

Authentication and Authorization with FAM (Forest Access Management)

What is FAM?

FAM (Forests Authorization Management) is an AWS Cognito-based authorization solution for the BC Natural Resource Sector. It provides a modern, standards-based approach to authentication and authorization, replacing legacy systems like WebADE and ADAM. FAM enables business users to define and assign groups and roles, and integrates with OIDC to securely include authorization context in authentication workflows.

Why use FAM?

FAM was designed to address the limitations of previous authorization systems:

  • It offers a business-friendly interface for managing access, unlike Keycloak or legacy tools.
  • Authorization data is managed securely and can be accessed via APIs for automation.
  • FAM supports the OpenID Connect (OIDC) standard, ensuring that authorization information is included in JWTs and can be used by modern digital products and services.
  • Forest clients can manage their own staff access to FSA applications.

How to configure FAM

To onboard your application with FAM, follow the official onboarding instructions in their FAM Onboarding documentation

How to set up authentication

FAM authentication requires you to provide environment variables for your AWS Cognito User Pool and Client ID. You can do this by creating a .env file in your project root, or by passing them as environment variables at runtime.

Example .env file:

VITE_USER_POOLS_ID=ca-central-1_<USER_POOL_ID>
VITE_USER_POOLS_WEB_CLIENT_ID=<CLIENT_ID>
VITE_MOCK_AUTH=false

These variables are loaded in the application using the following code (see src/env.ts):

declare global {
 interface Window {
  config: object;
 }
}

export const env: Record<string, string> = { ...import.meta.env, ...window.config };

This allows you to override or supplement environment variables at runtime by setting window.config before your app loads.

Authentication modes (VITE_MOCK_AUTH)

This app supports two authentication paths:

  • VITE_MOCK_AUTH=false (default): app reads Cognito tokens through Amplify (fetchAuthSession).
  • VITE_MOCK_AUTH=true (test mode): app reads token claims from the Cognito cookie and parses JWT payload locally.

In mock mode, tests can replace the current user session by writing a new Cognito idToken cookie value for the configured VITE_USER_POOLS_WEB_CLIENT_ID.

Authentication Flow in this App

This frontend uses Amplify Auth (signInWithRedirect) with FAM/OIDC providers. The flow is:

  1. User clicks login on Landing page (IDIR or BCEIDBUSINESS).
  2. AuthProvider.login() calls signInWithRedirect(...) with the environment-specific provider name.
  3. User authenticates in FAM/Cognito.
  4. Browser returns to the app callback URL (often /dashboard?code=...&state=...).
  5. The app resolves session state via fetchAuthSession() and route guards/redirect components finalize navigation.

Because OAuth callback URLs and app deep links are different concerns, this app explicitly persists a return URL before login and restores it after callback.

Mock Authentication Flow for E2E

When VITE_MOCK_AUTH=true, E2E setup can skip interactive login and generate a mocked Cognito JWT session.

  1. Playwright setup project runs auth.setup.ts.
  2. Setup chooses mockAuthenticate (instead of authenticate) based on project metadata.
  3. mockAuthenticate calls mockJwt, which writes Cognito cookies for LastAuthUser and idToken.
  4. Storage state is saved to src/config/tests/user.<userType>.json and reused by E2E projects.

This flow keeps tests fast, deterministic, and independent from external IdP availability.

Overriding token claims in tests

Tests can override specific JWT claims (for example custom:idp_username) to validate profile rendering and authorization behavior.

import { mockJwt } from '@/config/tests/auth.helper';

test('profile info for BCeID', async ({ page }, testInfo) => {
    await mockJwt(page, testInfo.project.metadata, {
        'custom:idp_username': 'notame',
    });

    await page.getByRole('button', { name: 'Profile settings' }).click();
    await expect(page.getByText('BCEIDBUSINESS\\notame')).toBeVisible();
});

Return URL Preservation (Deep-Link Redirect)

To preserve query params for deep links (for example /search?district=DFN), the app stores a single redirect key in sessionStorage:

  • redirectAfterLogin

The three-function API in redirectStorage.ts (persistRedirectUrl, readPersistedRedirect, clearPersistedRedirect) wraps all storage access in try/catch to handle private-browsing quota failures silently.

Where this happens

  • routes/_authenticated.tsx (the pathless auth layout): captures pathname + searchStr via persistRedirectUrl() before redirecting unauthenticated users to /login.
  • routes/dashboard.tsx (DashboardRedirect): reads the persisted target once per mount (ref-guarded), clears it, and navigates the user to their intended destination.

Why only sessionStorage?

sessionStorage is ideal for tab-scoped post-login state. The previous four-key pattern across both sessionStorage and localStorage was simplified to a single key — the fallback localStorage keys were found to add complexity without meaningful resilience benefit.

Route Protection (Gating Routes)

Access control is structural in the file-based router: a route's position under src/routes/ decides which gate wraps it. Authentication and status checks live in the pathless _authenticated layout (src/routes/_authenticated.tsx), role restrictions in the tier layouts (_admin, _client-access), and feature flags in each route's beforeLoad. There are no guard HOCs and no per-route guard wiring.

For full details on the gate layouts, the beforeLoad flag gates, and how to add a gated route, see Routing and Route Guards.

Role validation and access-rule enforcement

In addition to simple authenticated/unauthenticated checks, this app enforces provider and role-specific access rules during route guarding. These rules are centralized in src/context/auth/userAccessValidation.ts (consumed through the pure computeAuthRedirect in src/routing/auth.ts) and can redirect users to:

  • /no-role (no assigned business role)
  • /unauthorized?reason=<RULE_CODE> (specific authorization-rule violation)

The unauthorized page resolves reason into a user-facing message. For full technical details on rule order, role semantics, and extension guidance, see Roles.

Example: a route restricted to any-of VIEWER/SUBMITTER lives inside the _client-access tier folder — no role declaration on the route itself:

// src/routes/_authenticated/_client-access/clients.tsx
export const Route = createFileRoute('/_authenticated/_client-access/clients')({
  component: MyClientListPage,
  staticData: {
    title: 'My clients',
    nav: { label: 'My clients', icon: Group, order: 10, to: '/clients' },
  },
});

The _client-access.tsx layout (one folder up) holds the REQUIRED_ROLES const and redirects users who lack the roles to /unauthorized. staticData.roles on the layout is nav metadata only.

Notes and Pitfalls

  • TanStack Router and Amplify do not automatically preserve your original deep-link query params end-to-end; you must persist/restore explicitly.
  • OAuth callback params (code, state) should not be treated as app filter params.
  • In development, Strict Mode can run effects twice; redirect logic should be idempotent (this is handled in routes/dashboard.tsx via a useRef guard).
  • For mocked auth in tests, VITE_USER_POOLS_WEB_CLIENT_ID must be available in Playwright project metadata (or env fallback), otherwise cookie keys will not match Cognito storage keys.

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally