Files
2026-05-05 19:36:25 +02:00

6.2 KiB

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 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
  • 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
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
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.

// 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.goStartCleanupLoop
  • 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
  • Location: pkg/user/magic_link_cleanup.goStartCleanupLoop
  • 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:
    docker compose up -d  # starts Postgres + Mailpit
    
  2. Inspect emails: http://localhost:8025 (Mailpit UI)
  3. HTTPS for OIDC (Phase B):
    make cert  # generates certs/dev-cert.pem + certs/dev-key.pem via mkcert
    
    See MKCERT.md for details.

Cross-references

Architecture Decision Records

ADR Description
ADR-0018 Original username/password auth system
ADR-0021 JWT secret retention and cleanup
ADR-0028 Passwordless migration (Phase A complete, Phase B in progress)
ADR-0029 Email infrastructure (Mailpit)
ADR-0030 BDD parallel email assertions

Documentation

Document Description
EMAIL.md SMTP setup and Mailpit usage
MKCERT.md Local HTTPS certificate setup
PHASE_B_ROADMAP.md Remaining OIDC work

Developer onboarding doc — see ADR-0028 for implementation details.