📝 docs: AUTH.md synthesis (Phase A complete, Phase B partial) #73
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨ `GET /api/v1/uptime` endpoint (PR #67) — returns server start_time and uptime_seconds
|
||||
- 📝 mkcert local HTTPS doc + Makefile `cert` target (PR #68) — prep for ADR-0028 Phase B OIDC callbacks
|
||||
- ✨ `pkg/auth/` skeleton for OpenID Connect (PR #69) — types + client surface, handlers come later (Phase B.3+)
|
||||
- 📝 ADR-0028 Phase B roadmap document (PR #71) — outlines remaining B.3 / B.4 / B.5 work
|
||||
|
||||
## [0.1.0] - 2026-05-05
|
||||
|
||||
### Added
|
||||
|
||||
132
documentation/AUTH.md
Normal file
132
documentation/AUTH.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Authentication System
|
||||
|
||||
## Overview
|
||||
|
||||
The dance-lessons-coach authentication system provides a passwordless magic-link flow as the primary mechanism, with legacy username+password support during the transition period. OpenID Connect (OIDC) integration is in progress for Phase B. See [ADR-0028](../adr/0028-passwordless-auth-migration.md) for the migration strategy.
|
||||
|
||||
## Authentication mechanisms supported
|
||||
|
||||
### Username + password (legacy, ADR-0018)
|
||||
- **Endpoint:** `POST /api/v1/auth/login`
|
||||
- **Status:** Operational, to be decommissioned in Phase C
|
||||
- **Details:** bcrypt-hashed passwords, JWT token issuance
|
||||
|
||||
### Magic link by email (ADR-0028 Phase A)
|
||||
- **Request endpoint:** `POST /api/v1/auth/magic-link/request` — accepts `{email}`, generates token, stores hash, sends email
|
||||
- **Consume endpoint:** `GET /api/v1/auth/magic-link/consume?token=<...>` — validates hash, marks consumed, issues JWT
|
||||
- **Always returns 200 on request** to prevent email enumeration
|
||||
- **First-link sign-up:** if email is unknown, consume endpoint creates the user record
|
||||
|
||||
### OpenID Connect (ADR-0028 Phase B, work in progress)
|
||||
- **Status:** Skeleton merged (`pkg/auth/`), handlers and flow not yet wired
|
||||
- **Planned endpoints:**
|
||||
- `GET /api/v1/auth/oidc/start` — generates state + PKCE, redirects to provider
|
||||
- `GET /api/v1/auth/oidc/callback` — exchanges code for tokens, validates id_token, issues internal JWT
|
||||
- **Provider config:** `auth.oidc.providers.*` in config
|
||||
|
||||
## Magic-link flow detail
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
User->>Server: POST /api/v1/auth/magic-link/request {email}
|
||||
Server-->>User: 200 (always — anti-enumeration)
|
||||
Server->>Mailpit (or SMTP provider): SMTP send "Your sign-in link"
|
||||
User->>Email: clicks link
|
||||
User->>Server: GET /api/v1/auth/magic-link/consume?token=<plain>
|
||||
Server->>DB: verify hash, mark consumed, ensure user exists
|
||||
Server-->>User: 200 {token: <JWT>}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Email (ADR-0029)
|
||||
| Config key | Env var | Default | Description |
|
||||
|------------|---------|---------|-------------|
|
||||
| `auth.email.from` | `DLC_AUTH_EMAIL_FROM` | `noreply@dance-lessons-coach.local` | Sender address |
|
||||
| `auth.email.smtp_host` | `DLC_AUTH_EMAIL_SMTP_HOST` | `localhost` | SMTP host |
|
||||
| `auth.email.smtp_port` | `DLC_AUTH_EMAIL_SMTP_PORT` | `1025` | SMTP port |
|
||||
| `auth.email.smtp_use_tls` | `DLC_AUTH_EMAIL_SMTP_USE_TLS` | `false` | Use TLS |
|
||||
| `auth.email.timeout` | `DLC_AUTH_EMAIL_TIMEOUT` | `10s` | Connection timeout |
|
||||
|
||||
### Magic link (ADR-0028 Phase A)
|
||||
| Config key | Env var | Default | Description |
|
||||
|------------|---------|---------|-------------|
|
||||
| `auth.magic_link.ttl` | `DLC_AUTH_MAGIC_LINK_TTL` | `15m` | Token lifetime |
|
||||
| `auth.magic_link.base_url` | `DLC_AUTH_MAGIC_LINK_BASE_URL` | `http://localhost:8080` | Base URL for links |
|
||||
| `auth.magic_link.cleanup_interval` | `DLC_AUTH_MAGIC_LINK_CLEANUP_INTERVAL` | `1h` | Cleanup loop interval |
|
||||
|
||||
### JWT (ADR-0021)
|
||||
| Config key | Env var | Default | Description |
|
||||
|------------|---------|---------|-------------|
|
||||
| `auth.jwt.ttl` | `DLC_AUTH_JWT_TTL` | `1h` | Token time-to-live |
|
||||
| `auth.jwt.secret_retention.retention_factor` | `DLC_AUTH_JWT_SECRET_RETENTION_FACTOR` | `2.0` | Retention multiplier |
|
||||
| `auth.jwt.secret_retention.max_retention` | `DLC_AUTH_JWT_SECRET_MAX_RETENTION` | `72h` | Maximum retention |
|
||||
| `auth.jwt.secret_retention.cleanup_interval` | `DLC_AUTH_JWT_SECRET_CLEANUP_INTERVAL` | `1h` | Secret cleanup interval |
|
||||
|
||||
### OIDC (Phase B, prep)
|
||||
| Config key | Env var | Default | Description |
|
||||
|------------|---------|---------|-------------|
|
||||
| `auth.oidc.providers.<name>.issuer_url` | `DLC_AUTH_OIDC_ISSUER_URL` | - | Provider issuer URL |
|
||||
| `auth.oidc.providers.<name>.client_id` | `DLC_AUTH_OIDC_CLIENT_ID` | - | Client ID |
|
||||
| `auth.oidc.providers.<name>.client_secret` | `DLC_AUTH_OIDC_CLIENT_SECRET` | - | Client secret |
|
||||
|
||||
## Token model
|
||||
|
||||
Magic-link tokens use **SHA-256 hex hashing at rest** — only the hash is stored in the database (`token_hash` column, 64 chars). The plaintext token is emailed to the user and must be supplied back to re-derive the hash. This means a database leak reveals no usable tokens. See `pkg/user/magic_link.go` for the rationale.
|
||||
|
||||
```go
|
||||
// HashMagicLinkToken returns the lowercase hex sha256 of token
|
||||
func HashMagicLinkToken(plaintext string) string {
|
||||
sum := sha256.Sum256([]byte(plaintext))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
```
|
||||
|
||||
## Cleanup loops
|
||||
|
||||
### JWT secret retention (ADR-0021)
|
||||
- **Location:** `pkg/user/jwt_manager.go` — `StartCleanupLoop`
|
||||
- **Interval:** Configurable via `auth.jwt.secret_retention.cleanup_interval` (default: 1h)
|
||||
- **Behavior:** Removes secrets older than retention period (TTL x retention_factor, capped at max_retention)
|
||||
- **Safety:** Never removes the current primary secret
|
||||
|
||||
### Magic-link expired tokens (ADR-0028 Phase A)
|
||||
- **Location:** `pkg/user/magic_link_cleanup.go` — `StartCleanupLoop`
|
||||
- **Interval:** Configurable via `auth.magic_link.cleanup_interval` (default: 1h)
|
||||
- **Behavior:** Deletes tokens where `expires_at < now`
|
||||
- **Implementation:** Calls `DeleteExpiredMagicLinkTokens` on the repository
|
||||
|
||||
## Local dev setup
|
||||
|
||||
1. **Start services:**
|
||||
```bash
|
||||
docker compose up -d # starts Postgres + Mailpit
|
||||
```
|
||||
2. **Inspect emails:** http://localhost:8025 (Mailpit UI)
|
||||
3. **HTTPS for OIDC (Phase B):**
|
||||
```bash
|
||||
make cert # generates certs/dev-cert.pem + certs/dev-key.pem via mkcert
|
||||
```
|
||||
See [MKCERT.md](MKCERT.md) for details.
|
||||
|
||||
## Cross-references
|
||||
|
||||
### Architecture Decision Records
|
||||
| ADR | Description |
|
||||
|-----|-------------|
|
||||
| [ADR-0018](../adr/0018-user-management-auth-system.md) | Original username/password auth system |
|
||||
| [ADR-0021](../adr/0021-jwt-secret-retention-policy.md) | JWT secret retention and cleanup |
|
||||
| [ADR-0028](../adr/0028-passwordless-auth-migration.md) | Passwordless migration (Phase A complete, Phase B in progress) |
|
||||
| [ADR-0029](../adr/0029-email-infrastructure-mailpit.md) | Email infrastructure (Mailpit) |
|
||||
| [ADR-0030](../adr/0030-bdd-email-parallel-strategy.md) | BDD parallel email assertions |
|
||||
|
||||
### Documentation
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [EMAIL.md](EMAIL.md) | SMTP setup and Mailpit usage |
|
||||
| [MKCERT.md](MKCERT.md) | Local HTTPS certificate setup |
|
||||
| [PHASE_B_ROADMAP.md](PHASE_B_ROADMAP.md) | Remaining OIDC work |
|
||||
|
||||
---
|
||||
|
||||
*Developer onboarding doc — see ADR-0028 for implementation details.*
|
||||
Reference in New Issue
Block a user