> ## 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 flow

> How authentication works across the system.

```mermaid theme={null}
sequenceDiagram
    participant User as User (iOS)
    participant App as iOS App
    participant Cognito as AWS Cognito
    participant API as Backend API
    participant MW as Auth Middleware
    participant JWKS as Cognito JWKS

    Note over User,JWKS: Signup flow

    User->>App: Enter email + password
    App->>Cognito: SignUp (email, password)
    Cognito-->>App: Confirmation required
    Cognito->>User: Verification code (email)
    User->>App: Enter verification code
    App->>Cognito: ConfirmSignUp (code)
    Cognito-->>App: User confirmed

    Note over User,JWKS: Signin flow

    User->>App: Enter email + password
    App->>Cognito: InitiateAuth (email, password)
    Cognito-->>App: JWT access token + refresh token
    App->>App: Store tokens locally (Keychain)

    Note over User,JWKS: Post-login sync

    App->>API: POST /devices {pushToken, deviceId, osVersion, appVersion}
    API-->>App: 200 OK (device registered)
    App->>API: GET /user/follows
    API-->>App: List of active follow records
    App->>App: Reconcile local followedMatchIds with server state

    Note over User,JWKS: Authenticated API call

    App->>API: Request with Bearer token
    API->>MW: Validate token
    MW->>JWKS: Fetch Cognito public keys (cached)
    JWKS-->>MW: Public keys
    MW->>MW: Verify JWT signature + expiry
    MW->>MW: Extract user ID, groups
    alt Token valid
        MW-->>API: Authenticated user context
        API-->>App: 200 Response
    else Token expired/invalid
        MW-->>App: 401 Unauthorized
        App->>Cognito: Refresh token flow
        Cognito-->>App: New access token
    end

    Note over User,JWKS: Admin access

    MW->>MW: Check cognito:groups claim
    alt Groups contain 'admin' or 'testing'
        MW-->>API: Admin context granted
    else No admin group
        MW-->>App: 403 Forbidden
    end

    Note over User,JWKS: API Key access (admin/automation)

    App->>API: Request with X-API-Key header
    API->>MW: Check API key
    MW->>MW: Validate against stored keys
    MW-->>API: Bypasses Cognito, grants access
```

## How it works

Authentication uses **AWS Cognito** as the identity provider. The iOS app communicates directly with Cognito for signup and signin, then uses the resulting JWT tokens for all API calls.

### Signup

Standard email/password signup through Cognito. The user receives a verification code via email and must confirm before the account is active. No custom backend logic is involved in signup -- it's entirely Cognito-managed.

### Signin

The app calls Cognito's `InitiateAuth` with email and password. On success, Cognito returns:

* **Access token** (JWT, short-lived) -- used for API authorization
* **Refresh token** (long-lived) -- used to get new access tokens without re-entering credentials

Tokens are stored in the iOS Keychain.

### Post-login sync

After successful signin, the app performs two sync operations:

<Steps>
  <Step title="Device registration">
    `POST /devices` registers the device push token, `deviceId`, OS version, and app version. This ensures the backend can send push notifications to this device. See [device registration](/business-flows/device-registration).
  </Step>

  <Step title="Follow sync">
    `GET /user/follows` fetches the user's active match follow records. The app reconciles this with its local `followedMatchIds` set in `UserDefaults`. This handles follows created on another device, state lost during reinstall, and follows that expired while signed out.
  </Step>
</Steps>

<Tip>
  The follow sync is critical for multi-device support. If a user follows a match on Device A and then opens the app on Device B, the `GET /user/follows` call on login ensures Device B shows the correct follow state and can request Live Activities for those matches.
</Tip>

### JWT validation

The backend auth middleware validates every request:

<Steps>
  <Step title="Extract token">
    Pulls the Bearer token from the Authorization header.
  </Step>

  <Step title="Verify signature">
    Validates the JWT against Cognito's JWKS (JSON Web Key Set) public keys. The keys are cached locally to avoid hitting Cognito on every request.
  </Step>

  <Step title="Check expiry">
    Rejects expired tokens with 401.
  </Step>

  <Step title="Extract claims">
    Pulls user ID, email, and group memberships from the token claims.
  </Step>
</Steps>

### Admin access

Admin endpoints check the `cognito:groups` claim in the JWT. A user must belong to either the `admin` or `testing` group in Cognito to access admin routes.

<Note>
  Group membership is managed directly in the AWS Cognito console or via AWS CLI. There is no self-service admin promotion.
</Note>

### API Key access

For automation and admin tooling, the backend also supports an `X-API-Key` header. This bypasses Cognito entirely -- the key is validated against a stored set of keys. Use this for server-to-server calls or CI/CD pipelines.

<Warning>
  API keys grant full access and bypass user-level authorization. Treat them like root credentials. They should never be embedded in client apps.
</Warning>

### Dev mode

When running locally with auth disabled, the middleware skips all validation and returns hardcoded dev users. This is controlled by environment configuration and should never be active in production.

### Failure modes

| Scenario                 | Behavior                                                                  |
| ------------------------ | ------------------------------------------------------------------------- |
| Expired access token     | 401 returned, iOS auto-refreshes via refresh token                        |
| Expired refresh token    | 401 returned, user must re-signin                                         |
| Cognito JWKS unreachable | Middleware uses cached keys; if cache is cold, all requests fail with 500 |
| Invalid API key          | 401 returned                                                              |

## Areas of improvement

<Warning>
  **API key comparison is not constant-time.** In `adminAuth.ts` line 66, the API key is compared with `providedKey === ADMIN_API_KEY` (strict equality). This is vulnerable to timing attacks -- an attacker can probe one character at a time by measuring response latency. Use `crypto.timingSafeEqual` (with a length pre-check) for the comparison.
</Warning>

<Warning>
  **Dev mode bypasses auth based on `NODE_ENV` alone.** In `auth.ts` lines 77-81, when `NODE_ENV === 'development'`, the middleware returns a hardcoded `dev-user` and skips all JWT validation. The same pattern exists in `adminAuth.ts` line 77-80 with a hardcoded `dev-admin`. If `NODE_ENV` is accidentally set to `development` in production (e.g., a misconfigured deploy), all endpoints become unauthenticated. Consider requiring an explicit `DISABLE_AUTH=true` flag separate from `NODE_ENV`.
</Warning>

<Warning>
  **`requireAdmin` does not accept the `testing` group, but `requireAuth` does.** In `auth.ts` line 104, `isAdmin` is set to `true` for users in either `admin` or `testing` groups. However, `requireAdmin` in `adminAuth.ts` line 99 only checks for `groups.includes('admin')` -- it rejects `testing` group members with 403. This inconsistency means a user can appear as `isAdmin: true` in `req.user` (via `requireAuth`) but be denied access to admin routes (via `requireAdmin`). Either both should accept `testing` or neither should.
</Warning>

<Note>
  **Cognito JWKS cold-cache failure is unhandled.** The `aws-jwt-verify` library caches JWKS keys internally, but on a cold start (fresh deploy, new instance), the first request triggers a network call to Cognito's JWKS endpoint. If that call fails (e.g., transient network issue), every request returns 500 until the next attempt succeeds. There is no pre-warming of the JWKS cache at server startup.
</Note>

<Note>
  **iOS `getAccessToken()` does not explicitly handle token refresh.** In `CognitoAuthService.swift` `getAccessToken()`, the method calls `Amplify.Auth.fetchAuthSession()` which Amplify internally handles refresh for. However, if the refresh token itself has expired (e.g., user inactive for 30+ days with default Cognito settings), this throws an error that bubbles up as a generic `AuthError`. The iOS app has no dedicated handling to detect "refresh token expired" and redirect to the sign-in screen -- the user sees a cryptic error instead.
</Note>

<Note>
  **Duplicate `CognitoJwtVerifier` instances.** Both `auth.ts` and `adminAuth.ts` create their own lazy-initialized `CognitoJwtVerifier` with identical configuration (same `userPoolId`, `clientId`, `tokenUse`). This means two separate JWKS caches and two separate HTTP connections to Cognito's JWKS endpoint. Extract the verifier into a shared module to deduplicate.
</Note>

<Note>
  **`pendingPassword` stored in memory during sign-up flow.** In `CognitoAuthService.swift`, the user's plaintext password is held in `pendingPassword` so it can be used for auto-sign-in after confirmation. It is cleared on `signOut()` and `resetState()`, but if the user abandons the verification flow without completing it or signing out, the password remains in memory for the lifetime of the `CognitoAuthService` instance.
</Note>

## Related pages

* [Device registration](/business-flows/device-registration) -- POST /devices happens immediately after signin
* [Match following](/business-flows/match-following) -- GET /user/follows syncs follow state on login
