# cx Authentication & Access Control

_Status: implemented and deployed — cx-auth.js v1.3.5_

---

## 1. Roles

| Role | Who | Scope |
|---|---|---|
| `public` | Anyone | Open content; no authentication required |
| `user` | Org audience | Display pages — open by default; token-protected per org if `config.json` sets `access: token` |
| `client` | Org operator | Per-org; managed by contexts.live admin |
| `admin` | contexts.live staff | System-wide; all orgs, all tools |

---

## 2. What Each Role Can Access

### public
- All `cx/lib/`, `cx/css/`, `cx/fonts/` assets
- All `cx/user/` display pages (via org symlink)
- All org `config.json`, `packs/`, `media/`

### client
- Org dashboard (`/org/{slug}/`)
- Org tools — Subtitle Editor, Pack Migrator, Pack Merge (`/org/{slug}/tools/`)
- Org client pages — control pages, client-only display (`/org/{slug}/client/`)
- A client may administer more than one org; each is granted separately by admin

### admin
- System admin dashboard (`/cx/cx-admin-dashboard.html`)
- All cx tools (`/cx/tools/`)
- All org dashboards and client pages

---

## 3. Architecture

`auth.js` acts as both:

- **Oracle** — answers nginx `auth_request` subrequests: can this session access this path?
- **Auth server** — handles login, logout, and user management

nginx enforces access by asking the oracle on every protected request. All logic lives in auth.js. No htpasswd files required for new paths.

```
Browser
  │
  ├─ GET /org/nzo/client/cx-control-list.html
  │     nginx → auth_request → auth.js /auth-check
  │               ├─ valid session, client role, org=nzo in user.orgs → 200 → serve file
  │               └─ no cookie / wrong org / insufficient role → 401 → nginx @login
  │
  ├─ GET /cx/tools/cx-login.html
  │     nginx → public (exempt from tools protection)
  │
  └─ POST :8010/auth/login
        auth.js → validate credentials → set cookie → redirect to original URL
```

auth.js runs as a separate Node.js HTTPS process on port 8010, managed by PM2 (`cx-auth`).

---

## 4. Session Model

Sessions use a signed cookie — no server-side session store.

- Cookie name: `cx-session`
- Signed with HMAC-SHA256 using `AUTH_SECRET` environment variable
- No expiry — persists until user logs out or clears cookies
- Payload: `{ email, role, orgs[], iat }`

On each `auth_request`, auth.js:
1. Reads the `cx-session` cookie
2. Verifies the HMAC signature
3. Checks role rank and org list against the requested path
4. Returns 200 (allow) or 401 (deny)

No database lookup per request — everything is in the cookie.

---

## 5. Data Model

Users are stored in `services/cx-auth/cx-auth-users.json`:

```json
[
  {
    "email": "admin@example.com",
    "passwordHash": "...",
    "role": "admin",
    "orgs": ["*"]
  },
  {
    "email": "operator@example.com",
    "passwordHash": "...",
    "role": "client",
    "orgs": ["nzo", "bard"]
  }
]
```

- `role` — `admin`, `client`, or `user`
- `orgs` — slugs the user can access; `["*"]` means all orgs
- `passwordHash` — bcrypt hash

---

## 6. Access Rules

Defined in `ACCESS_RULES` in cx-auth.js. Evaluated in order; first match wins.

```
/cx/cx-admin-dashboard.html    role: admin
/cx/tools/*                    role: admin
/org/{slug}/client/*           role: client (org must be in user.orgs)
/org/{slug}/tools/*            role: client (org must be in user.orgs)
/org/{slug}/                   role: client (org must be in user.orgs)
/org/{slug}/user/*             open by default; token-protected if config.json sets access=token
(everything else)              open
```

Admin and client roles always bypass the token check — a valid session passes regardless.

---

## 7. nginx Configuration

### Oracle block

```nginx
location = /auth-check {
    internal;
    proxy_pass              https://127.0.0.1:8010/auth-check;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length   "";
    proxy_set_header        X-Original-URI   $request_uri;
    proxy_set_header        Cookie           $http_cookie;
    proxy_ssl_verify        off;
}

location @login {
    return 302 /cx/tools/cx-login.html?next=$request_uri;
}
```

### Protected locations

```nginx
location = /cx/cx-admin-dashboard.html {
    auth_request /auth-check;
    error_page 401 = @login;
}

location = /cx/tools/cx-login.html {
    # public — exempt from tools protection
}

location ^~ /cx/tools/ {
    auth_request /auth-check;
    error_page 401 = @login;
}

location ^~ /org/new/tools/ {
    auth_request /auth-check;
    error_page 401 = @login;
}

location ~ ^/org/[^/]+/client/ {
    auth_request /auth-check;
    error_page 401 = @login;
    autoindex on;
    autoindex_exact_size off;
    autoindex_format html;
}

location ~ ^/org/[^/]+/user/ {
    auth_request /auth-check;
    error_page 401 = @login;
    autoindex on;
    autoindex_exact_size off;
    autoindex_format html;
}
```

---

## 8. auth.js Endpoints

### Auth

```
GET  /auth-check         Oracle — returns 200 or 401
POST /auth/login         Validate credentials, set cookie, redirect
POST /auth/logout        Clear cookie, redirect to login page
GET  /health             Version check
```

### Management API (admin only)

```
GET    /admin/users              List all users
POST   /admin/users              Create user
PUT    /admin/users/:email       Update user (role, orgs, password)
DELETE /admin/users/:email       Remove user
```

---

## 9. Configuration

Config via `services/cx-auth/.env`:

```
AUTH_SECRET=...          HMAC signing secret (required)
AUTH_PORT=8010           Port (default 8010)
AUTH_HOST=https://contexts.live
CERTS_PATH=/etc/letsencrypt/live/contexts.live
SITE_ROOT=...            Absolute path to nginx root — used to read org config.json files
LOG_LEVEL=normal         verbose | normal | quiet
SENDGRID_API_KEY=...     Required for password reset (not yet implemented)
SENDGRID_FROM=...        Verified sender address
```

Log at `services/cx-auth/cx-auth.log`. PM2 name: `cx-auth`.

---

## 10. User Management UI

`/cx/tools/cx-user-admin.html` — admin only.

- List all users with role and org access
- Add / remove users
- Edit role and org list per user

Accessible from the admin dashboard under System Tools.

---

## 11. Cookie Consent

The `cx-session` cookie is strictly necessary — set only on active login, required for the service to function, not used for tracking. Under GDPR/UK PECR, strictly necessary cookies are exempt from consent requirements.

---

## 12. Token Access for User Pages

Orgs can restrict audience display pages (`/org/{slug}/user/`) to URL-token access by adding to `config.json`:

```json
{
    "access": "token",
    "token": "your-token-here"
}
```

Once set, user pages require `?token=your-token-here` in the URL. A valid admin or client session always passes regardless.

Token validation is handled by cx-auth.js reading the org's `config.json` from disk via the `SITE_ROOT` env var. The token check runs before the generic open-path return, so it cannot be bypassed.

To rotate a token: edit `config.json` — no server restart required (file is read on each request).

---

## 13. Not Yet Implemented

- Password reset via SendGrid (account set up and smoke-tested; reset flow not built)
