# Authentication

Legatus **never logs users in**. All identity is issued by **id.quizzman.com**.

## Token types

| Token | Audience (typical) | Principal |
|-------|--------------------|-----------|
| User access JWT | `legatus` | `Principal::User` |
| Service JWT | `legatus.s2s` | `Principal::Service` |
| Refresh token | n/a | Verified only via IdP introspect API |

## JWKS

- URL: `LEGATUS__AUTH__JWKS_URL` (IdP JWKS)
- Cached in-process with TTL (`jwks_cache_ttl`)
- Refresh on miss (key rotation) and on TTL expiry
- Algorithms: RS256 / ES256 (HS256 only for local dev via `hs256_secret`)

## Validation checks

1. Signature (JWKS / HS256)
2. `iss` == configured issuer
3. `aud` contains expected audience (user or service)
4. `exp` / `nbf` with clock skew (`clock_skew_secs`, default 30)
5. Optional `jti` replay set (in-memory; Redis-backed in hardening phase)

## Service-to-service

Every Quizzman service (Wiki, CMS, Play, Exam Engine, LMS, Dictionary, AI, Search) obtains a **service JWT** with:

- `iss`, `aud`, `exp`
- `sub` / `service` name
- `scope` / `roles`

Example scopes:

| Scope | Capability |
|-------|------------|
| `events:publish` | `POST /events` |
| `notifications:write` | Create notifications for arbitrary users |
| `webhooks:admin` | Manage webhooks |
| `broadcast:write` | System broadcast |

## Refresh verification

`RefreshTokenClient` POSTs to the IdP verify/introspect endpoint. Legatus does not store refresh tokens.

## Gateway usage

```http
Authorization: Bearer eyJ…
```

WebSocket: `?token=` or first-frame `{ "type": "auth", "token": "…" }`.

## Config reference

```toml
[auth]
issuer = "https://id.quizzman.com"
audience = "legatus"
service_audience = "legatus.s2s"
jwks_url = "https://id.quizzman.com/.well-known/jwks.json"
jwks_cache_ttl = "300s"
clock_skew_secs = 30
```
