# Architecture

## Overview

`@appdirect/auth-bff` implements the **Backend-for-Frontend (BFF)** authentication pattern for AppDirect marketplaces. The BFF sits between the browser and AppDirect's IdP, handling OAuth on the server and exposing a small set of HTTP endpoints to the frontend.

```mermaid
flowchart LR
    Browser --> BFF
    BFF --> IdP[AppDirect IdP]
    Browser -.->|never sees| Secret[client_secret]
    BFF --> Secret
```

## Layered design

The SDK has three layers:

### 1. Core (framework-agnostic)

- **`createAppDirectAuth(config)`** — factory that wires config, HTTP client, and flows
- **`flows/`** — pure async functions returning data (not HTTP responses)
- **`client.ts`** — all AppDirect HTTP calls
- **`session/`** — cookie helpers and FEJWT decode/expiry

No dependency on Next.js or Express. Can be used in any Node.js 18+ backend.

### 2. Adapters

Translate framework request/response objects to core flow calls:

| Export | Framework | Entry point |
|--------|-----------|-------------|
| `@appdirect/auth-bff/next` | Next.js App Router | `createNextAuthHandlers()` |
| `@appdirect/auth-bff/handlers` | Node route handlers | `createAuthRouteHandlers()` + cookie helpers |

Adapters handle:
- Reading cookies from request headers
- Setting/clearing cookies on responses
- HTTP redirects and JSON responses
- Method validation (405)

`@appdirect/auth-bff/handlers` also re-exports cookie utilities from `adapters/shared.ts` (`readAuthCookies`, `applyCookieMutations`, `createHandlerCookieWriter`) for custom BFF routes — the same helpers internal handlers use.

### 3. Consumer application

Thin route files that delegate to adapters. The consumer owns:
- Environment configuration
- Frontend UI and Global Header wiring
- Optional middleware for route protection

## Authentication flow

```mermaid
sequenceDiagram
    participant Browser
    participant BFF
    participant IdP as AppDirect_IdP

    Browser->>BFF: GET /api/auth/login
    BFF->>BFF: Generate state, set oauth_state cookie
    BFF->>IdP: Redirect to /oauth2/authorize
    IdP->>Browser: User authenticates
    IdP->>BFF: GET /api/auth/callback?code=...&state=...
    BFF->>BFF: Verify state cookie
    BFF->>IdP: POST /oauth2/token (authorization_code)
    IdP-->>BFF: access_token, refresh_token, id_token
    BFF->>IdP: POST /auth/token (Bearer oauth2 access_token)
    IdP-->>BFF: session access_token, auth refresh_token
    BFF->>IdP: GET /oauth2/userinfo (Bearer access_token)
    IdP-->>BFF: sub, email, name, picture
    BFF->>Browser: Set session + refresh_token cookies, redirect /
    Browser->>BFF: GET /api/auth/jwt
    BFF-->>Browser: { token: FEJWT }
```

## Session model: FEJWT-as-session

The SDK uses a single session strategy:

| Cookie | Contents | Purpose |
|--------|----------|---------|
| `session` | AppDirect **FEJWT** | User identity + Global Header token |
| `refresh_token` | Auth-layer refresh token from `/auth/token` | Renew expired session via `/auth/refresh` (not the OAuth2 refresh token) |
| `oauth_state` | Random hex (login only) | CSRF protection during OAuth redirect |

**Why FEJWT-as-session?**

- AppDirect Global Header expects a FEJWT via `/api/auth/jwt`
- Storing FEJWT directly avoids maintaining two parallel JWT formats
- `/api/auth/me` decodes FEJWT claims (`sub`, `email`, `name`, `picture`) and auto-refreshes when expired

**Not used in default flow:** locally signed session JWTs (`JWT_SECRET`). The reference example had a mismatch where callback stored FEJWT but `/me` tried to verify with a local secret. This SDK unifies on FEJWT decode with lazy refresh.

## Lazy refresh and single-flight

All session-aware endpoints (`/me`, `/jwt`, `/refresh`) use `ensureValidFeJwt()`:

1. If FEJWT in `session` cookie is valid → return it (no IdP call)
2. If expired (60s buffer before `exp`) → `POST /auth/refresh` (Bearer auth refresh_token), update session cookies
3. Concurrent requests on the same instance share one refresh via `RefreshCoordinator` (single-flight)

Refresh happens **at most ~once per FEJWT lifetime** (~5 minutes) per user session, only when an endpoint needs a valid token.

**Serverless note:** single-flight deduplicates within a warm Node instance. Different instances may each refresh once at the same expiry window — acceptable for v1.

## AppDirect-specific endpoints

Standard OIDC:

- `GET {issuer}/oauth2/authorize`
- `POST {issuer}/oauth2/token` (Basic auth with client credentials)
- `GET {issuer}/oauth2/userinfo`

AppDirect session layer (after OAuth2 login):

- `POST {issuer}/auth/token` — exchange OAuth2 `access_token` for **session access_token** (+ auth `refresh_token`)
- `POST {issuer}/auth/refresh` — renew session using auth-layer `refresh_token` as Bearer

Default scopes: `company openid profile email`

## Token refresh

When `/api/auth/me`, `/api/auth/jwt`, or `/api/auth/refresh` is called:

1. Read `session` cookie (session JWT) and `refresh_token` cookie (auth refresh token)
2. If session JWT missing or expired (60s buffer), call `POST /auth/refresh`
3. Update `session` and `refresh_token` cookies
4. Return user info or `{ token: session JWT }`

`/api/auth/me` applies the same refresh path so the UI stays consistent with Global Header without a separate `/jwt` call on page load.

## Security considerations

| Concern | Mitigation |
|---------|------------|
| Client secret exposure | Secret only on server; never sent to browser |
| Session hijacking | HttpOnly, SameSite=Lax cookies |
| CSRF on OAuth callback | `state` parameter verified against `oauth_state` cookie |
| XSS stealing tokens | HttpOnly cookies not accessible to JavaScript |
| Token in URL | Authorization code is one-time; tokens never in URL |

**Production:** set `secureCookies: true` (default when `NODE_ENV=production`) so cookies require HTTPS.

## Error handling

Core flows throw `AuthError` with structured `code`. Adapters map errors to:

- **Callback failures** → redirect to `/?error={code}`
- **API failures** (`/jwt`, `/refresh`) → JSON with `error` field and HTTP status

See [USAGE.md](USAGE.md) for error code reference.

## Extension points

- **Custom `fetch`** — pass `{ fetch: customFetch }` to `createAppDirectAuth` for testing or proxies
- **Custom routes** — `config.routes` overrides default `/api/auth/*` paths (used in error responses)
- **Custom scopes** — `config.scopes` array
- **Cookie options** — max ages, `secure`, `sameSite`, `path`
- **Cookie helpers** — `readAuthCookies`, `applyCookieMutations`, `createHandlerCookieWriter` from `@appdirect/auth-bff/handlers` for custom protected API routes

## Future adapters (out of scope v1)

- Next.js Pages Router (`./next-pages`)
- Express / Fastify middleware
- Edge runtime handlers
