Changeset 0.28.0.1 (#173)
This commit is contained in:
175
docs/ICD/auth.md
Normal file
175
docs/ICD/auth.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Auth
|
||||
|
||||
Three auth modes, configured via `AUTH_MODE` env var. All three issue the
|
||||
same JWT after authentication — downstream middleware is auth-mode-agnostic.
|
||||
|
||||
## Builtin Auth (default)
|
||||
|
||||
Username/password authentication with bcrypt hashing.
|
||||
|
||||
### Register
|
||||
|
||||
```
|
||||
POST /auth/register
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "jdoe",
|
||||
"password": "...",
|
||||
"email": "jdoe@example.com",
|
||||
"display_name": "Jane Doe"
|
||||
}
|
||||
```
|
||||
|
||||
Gated by `allow_registration` policy. Returns same shape as Login.
|
||||
A unique `handle` is auto-generated from `username` (collision-safe
|
||||
with `-2`, `-3` suffixes).
|
||||
|
||||
### Login
|
||||
|
||||
```
|
||||
POST /auth/login
|
||||
```
|
||||
|
||||
```json
|
||||
{ "username": "jdoe", "password": "..." }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJ...",
|
||||
"refresh_token": "opaque-string",
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"username": "jdoe",
|
||||
"email": "jdoe@example.com",
|
||||
"display_name": "Jane Doe",
|
||||
"handle": "jdoe",
|
||||
"role": "user",
|
||||
"auth_source": "builtin",
|
||||
"avatar_url": "/api/v1/profile/avatar?v=1234",
|
||||
"created_at": "...",
|
||||
"last_login": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh
|
||||
|
||||
```
|
||||
POST /auth/refresh
|
||||
```
|
||||
|
||||
```json
|
||||
{ "refresh_token": "opaque-string" }
|
||||
```
|
||||
|
||||
Returns new `access_token` and `refresh_token`. Old refresh token is
|
||||
invalidated (rotation).
|
||||
|
||||
### Logout
|
||||
|
||||
```
|
||||
POST /auth/logout
|
||||
```
|
||||
|
||||
```json
|
||||
{ "refresh_token": "opaque-string" }
|
||||
```
|
||||
|
||||
Revokes the refresh token. Returns `{ "message": "logged out" }`.
|
||||
|
||||
## mTLS Auth
|
||||
|
||||
Mutual TLS via reverse proxy headers. The proxy terminates TLS and
|
||||
forwards cert DN fields in headers. The backend auto-provisions
|
||||
users on first connection.
|
||||
|
||||
**Configured by:** `AUTH_MODE=mtls` + `MTLS_HEADER_*` env vars.
|
||||
|
||||
No explicit login/register endpoints. User identity is extracted from
|
||||
the TLS certificate on every request. `password_hash` is NULL for
|
||||
mTLS users.
|
||||
|
||||
**Headers parsed:**
|
||||
- `MTLS_HEADER_CN` (default: `X-SSL-Client-CN`) — common name → username
|
||||
- `MTLS_HEADER_DN` (default: `X-SSL-Client-DN`) — distinguished name → metadata
|
||||
- `MTLS_HEADER_VERIFY` (default: `X-SSL-Client-Verify`) — must be `SUCCESS`
|
||||
- `MTLS_HEADER_FINGERPRINT` (default: `X-SSL-Client-Fingerprint`) — stable identity
|
||||
|
||||
On first request with a valid cert, the system creates a user with
|
||||
`auth_source=mtls` and `external_id=fingerprint`.
|
||||
|
||||
## OIDC Auth
|
||||
|
||||
OpenID Connect authorization code flow. Tested with Keycloak but
|
||||
compatible with any OIDC-compliant IdP.
|
||||
|
||||
**Configured by:** `AUTH_MODE=oidc` + `OIDC_*` env vars.
|
||||
|
||||
### OIDC Login (redirect)
|
||||
|
||||
```
|
||||
GET /auth/oidc/login
|
||||
```
|
||||
|
||||
Redirects to the IdP authorization endpoint. Stores `state` + `nonce`
|
||||
in `oidc_auth_state` table (cleaned up after callback).
|
||||
|
||||
### OIDC Callback
|
||||
|
||||
```
|
||||
GET /auth/oidc/callback?code=...&state=...
|
||||
```
|
||||
|
||||
Exchanges authorization code for tokens, validates ID token signature
|
||||
via JWKS, extracts claims. Auto-provisions user on first login with
|
||||
`auth_source=oidc` and `external_id=sub`. Returns HTML fragment that
|
||||
passes tokens to the frontend via URL fragment.
|
||||
|
||||
**Claim mapping:**
|
||||
- `sub` → `external_id`
|
||||
- `preferred_username` → `username` + `handle`
|
||||
- `email` → `email`
|
||||
- `name` → `display_name`
|
||||
- `groups` → synced to internal groups with `source=oidc`
|
||||
|
||||
**Split-horizon issuer:** `OIDC_EXTERNAL_ISSUER_URL` can differ from
|
||||
`OIDC_ISSUER_URL` when the IdP's internal address (Docker/K8s service)
|
||||
differs from its external address.
|
||||
|
||||
**OIDC env vars:**
|
||||
|
||||
| Var | Required | Description |
|
||||
|-----|----------|-------------|
|
||||
| `OIDC_ISSUER_URL` | Yes | IdP issuer URL for discovery |
|
||||
| `OIDC_EXTERNAL_ISSUER_URL` | No | External issuer (if different from internal) |
|
||||
| `OIDC_CLIENT_ID` | Yes | Client ID |
|
||||
| `OIDC_CLIENT_SECRET` | Yes | Client secret |
|
||||
| `OIDC_REDIRECT_URL` | No | Callback URL (auto-derived if not set) |
|
||||
|
||||
## Login Page Adaptation
|
||||
|
||||
The `/login` page adapts by `AUTH_MODE`:
|
||||
|
||||
| Mode | UI |
|
||||
|------|-----|
|
||||
| `builtin` | Username/password form + register link |
|
||||
| `mtls` | Certificate status display |
|
||||
| `oidc` | "Sign in with SSO" button |
|
||||
|
||||
## User Identity Fields
|
||||
|
||||
All auth modes produce users with:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `auth_source` | `builtin`, `mtls`, or `oidc` |
|
||||
| `external_id` | IdP subject (OIDC) or cert fingerprint (mTLS). NULL for builtin. |
|
||||
| `handle` | Unique @mention handle. Auto-generated, collision-safe. |
|
||||
|
||||
`handle` is the canonical identifier for @mentions (replacing username
|
||||
in the resolution chain since v0.24.0).
|
||||
Reference in New Issue
Block a user