Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
133 lines
6.2 KiB
Markdown
133 lines
6.2 KiB
Markdown
# 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.*
|