# Login Process Documentation

This document describes the authentication flow in SteerEx, from initial page load to the dashboard view.

## Overview

SteerEx uses **Keycloak** as the identity provider for authentication. The frontend uses the `keycloak-js` adapter with a **check-sso** strategy, which checks for an existing session without forcing a login prompt.

Authentication state is managed through a single `loginState` value that can be one of:
- `inProgress` — Keycloak initialization is underway
- `loggedIn` — User is authenticated
- `loggedOut` — User is not authenticated (triggers Keycloak login)
- `errorInit` — Keycloak initialization failed
- `errorLoadUser` — User data could not be loaded from the backend
- `errorTokenRefresh` — Token refresh failed (session expired)
- `errorNoProgram` — User has no programs or program loading failed (used by `AuthErrorPage`, not set on `loginState`)

## Key Components

| Component         | File                                                                  | Purpose                                                              |
| ----------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- |
| AuthProvider      | `src/context-providers/auth-context-provider.tsx`                     | Manages `loginState`, Keycloak initialization, token refresh         |
| LoginPage         | `src/pages/login/login.page.tsx`                                      | Handles login flow, error redirects, and post-auth navigation        |
| AuthGuard         | `src/routes/_auth-routes.tsx`                                         | Protects routes, loads user programs, sets theme                     |
| AuthErrorPage     | `src/pages/error/auth-error.page.tsx`                                 | Displays auth error messages with "Try Again" and "Login Again"      |
| Program Wrapper   | `src/routes/_auth-routes/$programId.tsx`                              | DashboardWrapperPage layout for program routes                       |
| Program Index     | `src/routes/_auth-routes/$programId/index.tsx`                        | Redirects to first deliverable of program, or shows error            |
| Deliverable Index | `src/routes/_auth-routes/$programId/$deliverableId/index.lazy.tsx`    | Redirects to default tab (workstream)                                |
| Auth Error Route  | `src/routes/auth-error.tsx`                                           | `/auth-error` route with Zod-validated `type` search param           |
| Index Route       | `src/routes/index.tsx`                                                | Entry point, redirects to `/login`                                   |

## Authentication Flow

### Step 1: Initial Page Load

When a user visits the application:

1. **Index route (`/`)** immediately redirects to `/login`
2. **AuthProvider** initializes Keycloak with `check-sso` mode, setting `loginState` to `inProgress`
3. Keycloak checks for existing SSO session via silent iframe (`/silent-check-sso.html`)
4. On success: `loginState` becomes `loggedIn` (if authenticated) or `loggedOut` (if not)
5. On failure: `loginState` becomes `errorInit`

### Step 2: Login Page Logic

The `LoginPage` component is a non-visual component that reacts to `loginState` changes via a `useEffect`:

```
┌───────────────────────────────────────────────────────────────────────┐
│ LoginPage Effect (runs on loginState changes)                         │
├───────────────────────────────────────────────────────────────────────┤
│ 1. if (loginState === 'inProgress') → wait (show "Signing In...")     │
│ 2. if (loginState === 'loggedOut') → call login() → Keycloak          │
│ 3. if (loginState is an error) → navigate to /auth-error?type=...     │
│ 4. if (loggedIn && !viewerData) → wait for user data to load          │
│ 5. if (viewerData.roles includes 'unauthorized') → /unauthorized      │
│ 6. else → sanitize redirectUrl, then redirect to:                     │
│      redirectUrl (if safe) → /$programId → /auth-error?errorNoProgram │
└───────────────────────────────────────────────────────────────────────┘
```

**Redirect URL sanitization (step 6):** The `getSafeRedirectUrl()` helper rejects any URL that doesn't start with `/`, starts with `//`, or contains `://`. This prevents open-redirect attacks via crafted `redirectUrl` search params (e.g. `//evil.com`, `https://evil.com`).

**File:** [src/pages/login/login.page.tsx](../src/pages/login/login.page.tsx)

### Step 3: User Data Loading (AuthProvider)

After Keycloak authentication succeeds (`loginState === 'loggedIn'`):

1. **AuthProvider** fetches user data via `trpc.viewer.viewer` (enabled only when `loginState === 'loggedIn'`)
2. Backend returns: `{ user, userRoles, programs }`
3. User data is stored in auth context as `viewerData`
4. If fetch fails or returns an error: `loginState` is set to `errorLoadUser`

**File:** [src/context-providers/auth-context-provider.tsx](../src/context-providers/auth-context-provider.tsx)

### Step 4: Auth Route Guard

When accessing protected routes (`/_auth-routes/*`):

1. **Redirect effect**: If `loginState === 'loggedOut'` → navigate to `/login?redirectUrl=...` (preserves current URL for post-login redirect)
2. **Authentication gate**: If `loginState !== 'loggedIn'` → render nothing (`null`). This check runs before any data-dependent rendering to prevent flashing error/loading UI while the redirect effect navigates unauthenticated users to `/login`.
3. **Program loading**: Loads user programs via `useUserPrograms()`
4. **Error handling**: If program fetch fails (`isProgramError`) or user has no programs (`isLoaded && userPrograms.length === 0`) → render `<AuthErrorPage errorType="errorNoProgram" />` directly as a component (not a route redirect, because `isLoaded` becomes `true` two render cycles before `userPrograms` is populated)
5. **Program selection**:
   - Check localStorage for saved `programId`
   - If found and valid → use it
   - Otherwise → use first program from user's program list
6. **Theme setup**: Set brand theme via `setBrand(program.themeKey)`
7. **Render**: `<CommandPalette />` + `<Outlet />` for child routes

**File:** [src/routes/\_auth-routes.tsx](../src/routes/_auth-routes.tsx)

### Step 5: Program & Deliverable Redirects

After the auth guard renders successfully, index routes cascade to the final view:

#### `/$programId` (index)

1. Waits for `viewerData` and programs to load
2. If no programs → renders `<AuthErrorPage errorType="errorNoProgram" />` directly
3. Gets `rootDeliverableId` from `viewerData.programs[programId]`
4. Redirects to `/$programId/$deliverableId/workstream` via `<Navigate>`

**File:** [src/routes/\_auth-routes/$programId/index.tsx](../src/routes/_auth-routes/$programId/index.tsx)

#### `/$programId/$deliverableId` (index)

1. Gets `programId` and `deliverableId` from route params
2. Redirects to `/$programId/$deliverableId/workstream` via `<Navigate>`

**File:** [src/routes/\_auth-routes/$programId/$deliverableId/index.lazy.tsx](../src/routes/_auth-routes/$programId/$deliverableId/index.lazy.tsx)

## Auth Error Page

The `AuthErrorPage` component handles all authentication and program error states. It is used in two ways:

1. **As a routed page** at `/auth-error?type=...` — the `type` search param is validated with `z.enum(authErrorTypes)`
2. **As an inline component** rendered directly by `AuthGuard` and `ProgramRedirect` when programs can't be loaded

The page displays an error message based on the error type and provides two actions:

| Button        | Action                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------ |
| **Try Again** | Full page reload via `<a href="/">` — reinitializes all auth state from scratch                              |
| **Login Again** | Calls `logout({ redirectUri: window.location.origin })` — destroys the Keycloak SSO session, then redirects to app root which triggers the full login flow, showing the Keycloak login page |

The `logout()` function accepts an optional `{ redirectUri }` parameter that is forwarded to `keycloak.logout()`. Passing `window.location.origin` ensures Keycloak redirects back to the app root after session destruction, rather than back to `/auth-error` (which could cause a loop).

**Files:**
- [src/pages/error/auth-error.page.tsx](../src/pages/error/auth-error.page.tsx)
- [src/routes/auth-error.tsx](../src/routes/auth-error.tsx)

## Complete Flow Diagram

![Login Process Flow](./LOGIN_PROCESS.svg)

## Configuration

Authentication is configured via environment variables:

| Variable                         | Description                                    |
| -------------------------------- | ---------------------------------------------- |
| `VITE_AUTH`                      | Enable/disable authentication (`true`/`false`) |
| `VITE_PUBLIC_KEYCLOAK_URL`       | Keycloak server URL                            |
| `VITE_PUBLIC_KEYCLOAK_REALM`     | Keycloak realm name                            |
| `VITE_PUBLIC_KEYCLOAK_CLIENT_ID` | Keycloak client ID                             |
| `VITE_PUBLIC_CHECK_LOGIN_IFRAME` | Enable/disable SSO iframe check                |

### No-Auth Mode

When `VITE_AUTH=false`:

- `loginState` is set directly to `loggedIn`
- Keycloak initialization is skipped
- Useful for local development: `pnpm run dev:noAuth`

## Token Management

- **Token refresh**: Every 5 seconds, checks if token expires within 60 seconds (only when `loginState === 'loggedIn'`)
- **Token storage**: Bearer token stored in memory via `setAuthToken()`
- **Token header**: `Authorization: Bearer<token>` added to API requests

## Error Handling

Errors set `loginState` to an error type. The `LoginPage` detects error states and navigates to `/auth-error?type=<errorType>`. The `errorNoProgram` type is special — it is not set on `loginState` but is used directly by components that render `<AuthErrorPage>` inline.

| Scenario                     | `loginState`        | Behavior                                                              |
| ---------------------------- | ------------------- | --------------------------------------------------------------------- |
| Keycloak init fails          | `errorInit`         | LoginPage redirects to `/auth-error?type=errorInit`                   |
| User data fetch fails        | `errorLoadUser`     | LoginPage redirects to `/auth-error?type=errorLoadUser`               |
| User has `unauthorized` role | (stays `loggedIn`)  | LoginPage redirects to `/unauthorized`                                |
| Token refresh fails          | `errorTokenRefresh` | LoginPage redirects to `/auth-error?type=errorTokenRefresh`           |
| User has no programs         | (stays `loggedIn`)  | AuthGuard/ProgramRedirect renders `<AuthErrorPage>` inline            |
| Program fetch fails          | (stays `loggedIn`)  | AuthGuard renders `<AuthErrorPage>` inline                            |
| No programs (from LoginPage) | (stays `loggedIn`)  | LoginPage redirects to `/auth-error?type=errorNoProgram`              |
| Malicious `redirectUrl`      | (stays `loggedIn`)  | LoginPage discards URL, falls back to program or error page           |

## Route Structure

```
/                       → redirects to /login
/login                  → handles auth flow, error redirects, and post-auth navigation
/auth-error?type=...    → displays auth error messages (errorInit, errorLoadUser, errorTokenRefresh, errorNoProgram)
/unauthorized           → shown for users with 'unauthorized' role
/_auth-routes/          → protected layout (requires loginState === 'loggedIn')
  │                       - AuthGuard: redirects to /login if loggedOut
  │                       - renders null until loggedIn
  │                       - loads user programs, selects program, sets theme
  │                       - renders <AuthErrorPage> inline if no programs
  │
  ├── $programId/       → DashboardWrapperPage (layout wrapper)
  │   ├── index         → redirects to /$programId/$deliverableId/workstream
  │   │                   (gets deliverableId from viewerData.programs[programId])
  │   │                   or renders <AuthErrorPage> inline if no programs
  │   │
  │   └── $deliverableId/
  │       ├── index     → redirects to /$programId/$deliverableId/workstream
  │       │               (defaults to 'workstream' tab)
  │       │
  │       └── $tab      → workstream, pnl, one-off-costs, etc.
  │
  └── admin/            → admin pages
```

## Redirect URL Preservation

When an unauthenticated user tries to access a protected route:

1. AuthGuard detects `loginState === 'loggedOut'` and navigates to `/login?redirectUrl=...`
2. The current URL (path + search params) is stored as the `redirectUrl` search parameter
3. After successful authentication, LoginPage sanitizes the `redirectUrl` (must be a relative path starting with `/`, rejecting protocol-relative `//` and absolute URLs) and redirects to the saved URL
4. Fallback: If no `redirectUrl` (or if it was rejected as unsafe), redirects to `/$programId` (using programId from localStorage or first available program), which then cascades through index routes to the final view
5. If user has no programs at all, redirects to `/auth-error?type=errorNoProgram`
