> ## Documentation Index
> Fetch the complete documentation index at: https://285e39fd5e337e58f16290.sightscreen.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How auth works in the backend -- Cognito JWT, API keys, middleware chain, and Sentry integration.

## Overview

Sightscreen uses a two-track authentication system:

1. **Cognito JWT** -- for mobile app users and admin users
2. **Admin API Key** -- for server-to-server calls and internal tooling

Both tracks converge into a unified middleware chain that populates `req.user` before handlers execute. Authenticated user context is also attached to Sentry for error tracking.

***

## Cognito JWT validation

All authenticated mobile requests send a Bearer token in the `Authorization` header. The backend validates it using `aws-jwt-verify`.

```
Authorization: Bearer <Cognito ID token>
```

**Validation flow:**

1. Extract token from `Authorization: Bearer <token>` header.
2. Pass to `CognitoJwtVerifier` (from `aws-jwt-verify`) configured with the User Pool ID and Client ID.
3. The verifier fetches the JWKS (JSON Web Key Set) from Cognito and caches it.
4. On success, decode claims and build the user object.
5. On failure, return `401 Unauthorized`.

<Note>
  The JWKS is cached in-memory after the first fetch. Cold starts incur one extra network call to Cognito.
</Note>

### Required environment variables

| Variable               | Purpose                                         |
| ---------------------- | ----------------------------------------------- |
| `COGNITO_USER_POOL_ID` | The Cognito User Pool to validate against       |
| `COGNITO_CLIENT_ID`    | The App Client ID for token audience validation |

***

## Admin API Key

For server-to-server and internal tooling requests, the backend accepts a static API key.

```
X-API-Key: <value of ADMIN_API_KEY>
```

The key is compared directly against the `ADMIN_API_KEY` environment variable. If it matches, the request is treated as an admin-level request without needing a Cognito token.

<Warning>
  The API key grants full admin access. Rotate it if compromised. Never expose it in client-side code.
</Warning>

***

## Middleware chain

Four middleware functions control access across the route tree:

### `requireAuth`

Requires a valid Cognito JWT. Rejects with `401` if the token is missing or invalid. Populates `req.user`.

Used on all authenticated user endpoints (`/me`, `/matches`, `/devices`, etc.).

### `optionalAuth`

Attempts Cognito JWT validation but does **not** reject on failure. If a valid token is present, `req.user` is populated; otherwise `req.user` is `undefined`.

Used on endpoints that behave differently for authenticated vs. anonymous users.

### `requireAdmin`

Runs after `requireAuth`. Checks that `req.user.isAdmin` is `true` (i.e., the user belongs to the `admin` Cognito group). Rejects with `403 Forbidden` otherwise.

### `requireAdminOrApiKey`

Checks for **either**:

* A valid Cognito JWT with admin group membership, **or**
* A valid `X-API-Key` header

This is the gate on all `/admin/*` routes. It allows both human admins (via Pavilion) and automated systems (via API key) to manage resources.

```
Incoming request
  |
  +- Has Authorization header?
  |    +- Yes -> validate JWT -> populate req.user
  |    +- No  -> check X-API-Key header
  |               +- Valid key -> grant admin access
  |               +- No key   -> 401 Unauthorized
  |
  +- Route requires admin?
       +- Yes -> check req.user.isAdmin or valid API key
       +- No  -> proceed to handler
```

***

## Sentry integration

After successful authentication, the middleware sets the Sentry user context so that all errors and transactions for the request are tagged with the user identity:

```ts theme={null}
Sentry.setUser({
  id: req.user.userId,
  segment: req.user.isAdmin ? "admin" : "user",
});
```

This enables filtering errors by user in the Sentry dashboard and correlating issues with specific accounts. The Sentry context is cleared at the end of each request lifecycle.

***

## User object shape

After successful authentication, `req.user` contains:

<ResponseField name="userId" type="string" required>
  The Cognito `sub` claim. Unique identifier for the user.
</ResponseField>

<ResponseField name="phone" type="string" required>
  The user's phone number from Cognito claims.
</ResponseField>

<ResponseField name="isAdmin" type="boolean" required>
  `true` if the user belongs to the `admin` Cognito group.
</ResponseField>

<ResponseField name="groups" type="string[]" required>
  List of Cognito groups the user belongs to (e.g., `["admin"]`).
</ResponseField>

```json theme={null}
{
  "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "phone": "+61400000000",
  "isAdmin": true,
  "groups": ["admin"]
}
```

***

## Dev mode bypass

<Warning>
  Only active when `NODE_ENV=development`. Never runs in production.
</Warning>

When running locally, the authentication middleware skips Cognito validation entirely and injects a hardcoded user object:

| Scenario  | Trigger                                            | Injected user                                                                      |
| --------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Dev user  | Any request with `Authorization: Bearer dev`       | `{ userId: "dev-user", phone: "+61400000000", isAdmin: false, groups: [] }`        |
| Dev admin | Any request with `Authorization: Bearer dev-admin` | `{ userId: "dev-admin", phone: "+61400000000", isAdmin: true, groups: ["admin"] }` |

This lets you test endpoints locally without running Cognito infrastructure.
