00004_recovery_and_sessions.sql 1.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041
  1. -- 00004_recovery_and_sessions.sql
  2. --
  3. -- Auth-A v3 rewrite: replace synthetic-identity-based recovery with a custom
  4. -- recovery_codes table (Argon2id hashed + HMAC-SHA256 prefix index for O(1)
  5. -- lookup) and a user_sessions table for server-side session tracking
  6. -- (revocation + 30d absolute cap).
  7. --
  8. -- See: research/PLAN-AUTH-A.md (v3) and research/COOKIE-SHAPE-DECISION.md.
  9. ALTER TABLE public.users DROP COLUMN IF EXISTS recovery_code;
  10. CREATE TABLE public.recovery_codes (
  11. user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  12. argon2_hash text NOT NULL,
  13. prefix_hmac bytea NOT NULL, -- HMAC-SHA256(pepper, code) truncated to first 8 bytes
  14. created_at timestamptz NOT NULL DEFAULT now()
  15. );
  16. CREATE INDEX idx_recovery_codes_prefix ON public.recovery_codes(prefix_hmac);
  17. ALTER TABLE public.recovery_codes ENABLE ROW LEVEL SECURITY;
  18. -- No policies; service role only.
  19. COMMENT ON TABLE public.recovery_codes IS
  20. 'Argon2id-hashed recovery codes with HMAC prefix index. Service role only. Single-use; deleted on claim atomically via DELETE ... RETURNING.';
  21. CREATE TABLE public.user_sessions (
  22. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  23. user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  24. iat_original timestamptz NOT NULL DEFAULT now(),
  25. last_seen_at timestamptz NOT NULL DEFAULT now(),
  26. revoked_at timestamptz
  27. );
  28. CREATE INDEX idx_user_sessions_user_active ON public.user_sessions(user_id) WHERE revoked_at IS NULL;
  29. ALTER TABLE public.user_sessions ENABLE ROW LEVEL SECURITY;
  30. -- No policies; service role only.
  31. COMMENT ON TABLE public.user_sessions IS
  32. 'Server-side session records. JWT carries session_id; touch endpoint validates revoked_at IS NULL and now() - iat_original < 30d.';