# 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= Server->>DB: verify hash, mark consumed, ensure user exists Server-->>User: 200 {token: } ``` ## 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..issuer_url` | `DLC_AUTH_OIDC_ISSUER_URL` | - | Provider issuer URL | | `auth.oidc.providers..client_id` | `DLC_AUTH_OIDC_CLIENT_ID` | - | Client ID | | `auth.oidc.providers..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.*