Skip to main content

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.

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.
The JWKS is cached in-memory after the first fetch. Cold starts incur one extra network call to Cognito.

Required environment variables

VariablePurpose
COGNITO_USER_POOL_IDThe Cognito User Pool to validate against
COGNITO_CLIENT_IDThe 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.
The API key grants full admin access. Rotate it if compromised. Never expose it in client-side code.

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 the admin panel) 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

User object shape

After successful authentication, req.user contains:
userId
string
required
The Cognito sub claim. Unique identifier for the user.
phone
string
required
The user’s phone number from Cognito claims.
isAdmin
boolean
required
true if the user belongs to the admin Cognito group.
groups
string[]
required
List of Cognito groups the user belongs to (e.g., ["admin"]).
{
  "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "phone": "+61400000000",
  "isAdmin": true,
  "groups": ["admin"]
}

Dev mode bypass

Only active when NODE_ENV=development. Never runs in production.
When running locally, the authentication middleware skips Cognito validation entirely and injects a hardcoded user object:
ScenarioTriggerInjected user
Dev userAny request with Authorization: Bearer dev{ userId: "dev-user", phone: "+61400000000", isAdmin: false, groups: [] }
Dev adminAny 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.