veza/veza-backend-api/internal/config
senke 2026ffcb06 feat(auth): DB-backed JWT jti revocation ledger (sécu item 6)
The platform already had two revocation surfaces : Redis-backed
TokenBlacklist (token-hash keyed, T0174) and TokenVersion bump on the
user row (revokes ALL of a user's tokens). Both work but leave gaps :
  * Redis restart wipes the blacklist — a token revoked seconds before
    a Redis crash becomes valid again until natural expiry.
  * No way to revoke "session #3 of user X" from an admin UI : the
    blacklist is keyed by token hash, the admin doesn't have it.

This commit adds a durable, jti-keyed revocation ledger that closes
both gaps. The jti claim is already emitted on every access + refresh
token (services/jwt_service.go:155, RegisteredClaims.ID = uuid).

Schema (migrations/993_jwt_revocations.sql)
  * jwt_revocations(jti PK, user_id, expires_at, revoked_at, reason,
    revoked_by). PRIMARY KEY on jti = idempotent re-revoke. Indexes
    on user_id (admin "list my revocations") and expires_at (cleanup
    cron).

Service (internal/services/jwt_revocation_service.go)
  * NewJWTRevocationService(db, redisClient, logger) — Redis is
    optional cache.
  * Revoke(ctx, jti, userID, expiresAt, reason, revokedBy)
      - Redis SET (best-effort cache, TTL = remaining lifetime)
      - DB INSERT (durable record, idempotent via PK)
  * IsRevoked(ctx, jti)
      - Redis GET fast path
      - DB fallback on cache miss / Redis blip (fail-open : DB error
        is logged + treated as not-revoked, because the existing
        token-hash blacklist still protects).
      - Backfills Redis on DB hit so the next request hits cache.
  * ListByUser(ctx, userID, limit) — for the admin/user "active
    sessions" UI.
  * PurgeExpired(ctx, safetyMargin) — daily cron handle.

Middleware (internal/middleware/auth.go)
  * JTIRevocationChecker interface + SetJTIRevocationChecker setter.
  * After ValidateToken, in addition to the token-hash blacklist
    check, IsRevoked(claims.ID) is called. Either match = reject.
  * Nil-safe via reflect.ValueOf.IsNil() pattern matching the
    existing tokenBlacklist nil guard.

Wiring
  * config/services_init.go : always instantiate the service (DB
    required, Redis passed as nil if unavailable).
  * config/middlewares_init.go : SetJTIRevocationChecker on the auth
    middleware after construction.
  * config/config.go : new Config.JWTRevocationService field.

Logout flow (handlers/auth.go)
  * In addition to TokenBlacklist.Add(token, ttl), now calls
    JWTRevocationService.Revoke(jti, ...). Best-effort : the blacklist
    already protects the immediate-rejection path ; this just adds
    durability + a stable handle for admin tools.

Tests pass : go test ./internal/{handlers,services,middleware,core/auth}
              -short -count=1.

What v1.0.10 leaves to v2.1
  * /api/v1/auth/sessions/revoke/:jti  — admin-targeted endpoint.
    Service is ready ; the admin UI to drive it follows.
  * Daily PurgeExpired cron — call from a Forgejo workflow once
    per day with safetyMargin = 1h to keep table size bounded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:37:02 +02:00
..
config.go feat(auth): DB-backed JWT jti revocation ledger (sécu item 6) 2026-05-02 11:37:02 +02:00
config_test.go fix(ci): unblock CI red after v1.0.9 sprint 1 push (migration 986 + config tests) 2026-04-27 05:02:07 +02:00
cors.go feat: backend, stream server & infra improvements 2026-03-18 11:36:06 +01:00
db_init.go refactor(config): découper config.go par domaine (audit 2.7) 2026-02-15 14:44:33 +01:00
defaults.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
defaults_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
docs.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
docs_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_detection.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_detection_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_helpers.go feat(backend): OAuth FRONTEND_URL from config, docs update 2026-02-17 16:42:23 +01:00
env_loader.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
env_loader_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
middlewares_init.go feat(auth): DB-backed JWT jti revocation ledger (sécu item 6) 2026-05-02 11:37:02 +02:00
rabbitmq.go feat: backend — config, handlers, services, logging, migration 2026-03-23 15:46:57 +01:00
rate_limit.go feat: backend, stream server & infra improvements 2026-03-18 11:36:06 +01:00
redis_init.go feat(redis): Sentinel HA + cache hit rate metrics (W3 Day 11) 2026-04-28 13:36:55 +02:00
reloader.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
reloader_test.go v0.9.4 2026-03-05 23:03:43 +01:00
secrets.go [BE-SEC-014] be-sec: Implement secrets management 2025-12-24 12:30:18 +01:00
secrets_test.go STABILISATION: phase 3–5 – API contract, tests & chat-server hardening 2025-12-06 17:21:59 +01:00
services_init.go feat(auth): DB-backed JWT jti revocation ledger (sécu item 6) 2026-05-02 11:37:02 +02:00
testutils.go fix(backend): use explicit DISABLE_RATE_LIMIT_FOR_TESTS flag instead of env-based bypass 2026-02-15 15:56:53 +01:00
testutils_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
upload_limits.go feat(backend,web): single source of truth for upload-size limits 2026-04-16 19:37:37 +02:00
upload_limits_test.go feat(backend,web): single source of truth for upload-size limits 2026-04-16 19:37:37 +02:00
validation.go refactor(backend): split config into domain modules (P2) 2026-02-16 11:12:21 +01:00
validation_test.go fix(ci): unblock CI red after v1.0.9 sprint 1 push (migration 986 + config tests) 2026-04-27 05:02:07 +02:00
validator.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
validator_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
watcher.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00
watcher_test.go adding initial backend API (Go) 2025-12-03 20:29:37 +01:00