Skip to content

Conversation

@LaurenceJJones
Copy link
Member

@LaurenceJJones LaurenceJJones commented Nov 21, 2025

Pull Request: Refactor Captcha to Stateless JWT-Based Design

Summary

Refactors the captcha system from a stateful, UUID-based session design to a stateless, JWT-based signed cookie design. Removes all server-side session storage and consolidates cookie handling into the captcha package.

Key Changes

1. Stateless Token Design

  • Replaced in-memory session storage with stateless signed cookies
  • Implemented CaptchaToken with UUID, status, issued/expiration timestamps
  • All session state lives in client-side signed cookies
  • Sessions survive server restarts

2. JWT Migration

  • Migrated from custom payload.sig format to standard JWT (JWS) using golang-jwt/jwt/v4
  • Uses HMAC-SHA256 signing for token integrity
  • Automatic expiration handling via JWT library
  • Removed double-encoding (JWT is already base64url-encoded)

3. Code Consolidation

  • Moved CookieGenerator from internal/cookie to internal/remediation/captcha
  • Removed internal/cookie package (only used by captcha)
  • Removed internal/session package (no longer needed)
  • All cookie logic now in captcha package

4. Encapsulation & Configuration

  • Added methods on Captcha struct: NewPendingToken(), NewPassedToken(), GenerateCookie(), ValidateCookie()
  • Per-host configuration: pending_ttl, passed_ttl, cookie_secret
  • Removed SignCookies config (cookies always signed for stateless design)

5. Logic Improvements

  • Simplified redirect logic: only redirect on pending→valid transition (prevents loops)
  • Removed unnecessary flags: needsNewCookie, captchaStatus tracking
  • Better logging: single logger with host field set once
  • State transition tracking via initialStatus instead of boolean flags

6. HAProxy Integration

  • Updated HAProxy config to use %[url] for redirects (no server-side URL tracking)
  • Set redirect = "1" flag for HAProxy to handle redirects
  • Cookie management via HAProxy transaction variables

7. Backward Compatibility

  • Old format cookies automatically trigger new JWT cookie creation
  • Seamless migration: clients with old cookies get new JWT cookies on next request
  • No user-visible disruption during migration

Benefits

  • Stateless: No server-side storage, no GC needed, survives restarts
  • Standard: Uses JWT (RFC 7519) instead of custom format
  • Future-proof: Ready for JWE encryption and additional claims (e.g., client fingerprinting)
  • Simpler: Removed ~600 lines of code, cleaner architecture
  • Better traceability: UUID tracking throughout for debugging

Technical Details

  • Token Format: JWT with claims: uuid, st (status), iat, exp
  • Signing: HMAC-SHA256 (JWS)
  • Cookie: Session cookie (no Expires/MaxAge), HttpOnly, Secure, SameSite=Strict
  • Dependencies: Added github.com/golang-jwt/jwt/v4 as direct dependency

Testing

  • All existing tests pass
  • golangci-lint: 0 issues
  • Backward compatibility verified (old format → new JWT migration)

Breaking Changes

Haproxy configuration now handles the redirect based on the boolean flag
cookie_secret is now mandatory property that must be set by the user configuration on the host

- Replace stateful UUID-based session management with stateless signed cookies
- Implement CaptchaToken with UUID, status, issued/expiration timestamps
- Add SignCaptchaToken and ParseAndVerifyCaptchaToken for cookie signing
- Move token/cookie operations to Captcha struct methods (NewPendingToken, NewPassedToken, GenerateCookie, ValidateCookie)
- Add per-host configuration for pending_ttl, passed_ttl, and cookie_secret
- Remove global session manager and all session-related code
- Remove SignCookies configuration (cookies always signed for stateless design)
- Fix redirect loop by only redirecting on pending→valid transition
- Simplify logic by removing unnecessary flags (needsNewCookie, captchaStatus)
- Add UUID tracking throughout for better debugging and traceability
- Update HAProxy config to use %[url] for redirects instead of stored URL
- Clean up dead code and duplicated functionality
@LaurenceJJones LaurenceJJones added this to the 0.3.0 milestone Nov 21, 2025
- Move CookieGenerator struct and methods from internal/cookie to internal/remediation/captcha
- Remove internal/cookie package as it was only used by captcha
- All cookie-related functionality now lives in the captcha package
- Simplifies codebase by removing unnecessary package separation
- Add .vagrant/ to .gitignore
@LaurenceJJones LaurenceJJones force-pushed the refactor-captcha-stateless branch from 9b06567 to 5960d50 Compare November 21, 2025 10:54
- Replace custom payload.sig format with standard JWT using golang-jwt/jwt/v4
- Use HMAC-SHA256 signing (JWS) for token integrity
- Remove double-encoding: JWT tokens are already base64url-encoded
- Update ValidateCaptchaCookie to work directly with JWT format
- Maintain backward compatibility: old format cookies automatically trigger new JWT cookie creation
- Future-proof: JWT enables easy addition of encryption (JWE) and additional claims (e.g., client fingerprinting)

Benefits:
- Standard, well-audited format (RFC 7519)
- Automatic expiration handling via JWT library
- Ready for future encryption needs (JWE) for sensitive data
- Can easily add claims like IP hash, user-agent hash to prevent cookie reuse
@LaurenceJJones LaurenceJJones changed the title refactor: migrate captcha to stateless signed cookie design refactor: migrate captcha to stateless JWT-based design Nov 21, 2025
….3.0)

- Remove default fallback to secret_key for cookie_secret
- Require explicit cookie_secret configuration (breaking change)
- Enforce minimum 32 bytes for cookie_secret security
- Fail fast with clear error messages at initialization
- Update documentation to reflect required field
- Update example configs with cookie_secret requirement

This change ensures:
- Compliance: separate secrets for captcha provider and cookie signing
- Security: minimum 32 bytes enforced for cryptographic strength
- Multi-instance: explicit configuration required for shared cookies
- Clarity: clear error messages guide users to proper configuration
- Remove GetCookieSecret() getter method from Captcha struct
- Use direct property access (c.CookieSecret) in GenerateCookie() and ValidateCookie()
- Simplifies code by removing unnecessary abstraction layer
- Replace string concatenation with single strings in error messages
- Update README to use hex encoding for cookie_secret generation
- Use openssl rand -hex 32 for clearer byte count visibility
@LaurenceJJones LaurenceJJones force-pushed the refactor-captcha-stateless branch from 45246a2 to 593c2af Compare November 21, 2025 11:40
- Remove .vagrant files from git index (files remain on disk)
- .vagrant/ should be ignored via .gitignore (handled in separate PR)
- Merge main branch to include .gitignore changes (.vagrant/ addition)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants