Repository navigation
Authentication
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.
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.
To onboard your application with FAM, follow the official onboarding instructions in their FAM Onboarding documentation
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=falseThese 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.
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.
This frontend uses Amplify Auth (signInWithRedirect) with FAM/OIDC providers. The flow is:
- User clicks login on Landing page (
IDIRorBCEIDBUSINESS). -
AuthProvider.login()callssignInWithRedirect(...)with the environment-specific provider name. - User authenticates in FAM/Cognito.
- Browser returns to the app callback URL (often
/dashboard?code=...&state=...). - 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.
When VITE_MOCK_AUTH=true, E2E setup can skip interactive login and generate a mocked Cognito JWT session.
- Playwright setup project runs
auth.setup.ts. - Setup chooses
mockAuthenticate(instead ofauthenticate) based on project metadata. -
mockAuthenticatecallsmockJwt, which writes Cognito cookies forLastAuthUserandidToken. - Storage state is saved to
src/config/tests/user.<userType>.jsonand reused by E2E projects.
This flow keeps tests fast, deterministic, and independent from external IdP availability.
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();
});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.
-
routes/_authenticated.tsx(the pathless auth layout): capturespathname + searchStrviapersistRedirectUrl()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.
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.
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.
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.
- 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.tsxvia auseRefguard). - For mocked auth in tests,
VITE_USER_POOLS_WEB_CLIENT_IDmust be available in Playwright project metadata (or env fallback), otherwise cookie keys will not match Cognito storage keys.
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository