

GourdianToken
“Enterprise-grade JWT management for Go”
A production-ready JWT token management library for Go covering the full token lifecycle — generation, verification, revocation, and refresh-token rotation with replay-attack detection. Built from scratch over sixteen months with security-first principles, five pluggable storage backends, and eleven signing algorithms.
Tech Stack
Technologies & tools used
Backend
2Database
3Features & Highlights
Key capabilities and achievements
Full token lifecycle in one interface
CoreGourdianTokenMaker covers create, verify, revoke, and rotate for both access and refresh tokens — seven methods, one mental model, immutable and thread-safe after construction.
Refresh rotation with replay detection
CoreRotating a refresh token atomically invalidates the old one. A reused (replayed) token is detected and rejected, closing the classic stolen-refresh-token window.
Five pluggable storage backends
CoreOne TokenRepository interface with in-memory, Redis, GORM (Postgres/MySQL/SQLite), MongoDB, and a stateless no-storage mode — swap backends without touching call sites.
Eleven signing algorithms
CoreHMAC (HS256/384/512), RSA (RS256/384/512), RSA-PSS (PS256/384/512), ECDSA (ES256/384/512), and EdDSA (Ed25519), with an algorithm whitelist and the "none" algorithm hard-blocked.
Hard maximum-lifetime ceiling (mle claim)
A custom mle (max lifetime expiry) claim enforces an absolute expiry that survives any number of refreshes — sessions cannot be extended forever.
Sentinel errors for errors.Is
ErrTokenRevoked, ErrTokenRotated, ErrTokenExpired, ErrInvalidSignature and friends let callers branch on failure modes without string matching.
Tokens stored as SHA-256 hashes
Revocation and rotation state stores never hold raw tokens — a leaked storage backend does not leak usable credentials.
Managed background cleanup
Expired revocation and rotation records are purged by context-driven cleanup goroutines with a configurable interval and an idempotent Close() for clean shutdown.
System Architecture
One core, one storage contract, five implementations
The library is split into a core JWTMaker (signing, claim validation, key management) and a TokenRepository storage contract that isolates all state. The maker never talks to a database directly — revocation and rotation state flow through the repository interface, so backends are interchangeable and a stateless mode simply omits the repository. The crucial repository method is MarkTokenRotatedAtomic, a compare-and-swap primitive that each backend implements with its own native mechanism, keeping rotation race-free everywhere. Tokens are hashed with SHA-256 before storage, and background cleanup goroutines purge expired records until Close() is called.
Architecture Diagrams
System Overview

The JWTMaker core routes all revocation and rotation state through the TokenRepository contract, with five interchangeable backend implementations.
Components
JWTMaker core
ServiceImmutable, thread-safe token engine: creation, verification, claim validation, and cryptographic key loading for all eleven algorithms.
- Sign and verify access/refresh tokens
- Validate configuration and enforce algorithm whitelist
- Enforce the mle max-lifetime ceiling
- Load and validate symmetric keys and PEM key files (0600 permissions)
Redis repository
DatabaseProduction-recommended backend using SETNX for atomic rotation marks and native TTLs for expiry.
- Atomic compare-and-swap rotation marks via SETNX
- TTL-based expiry with a 100ms floor against near-zero-TTL races
- Fast revocation checks on the hot verify path
GORM repository
DatabaseSQL backend for Postgres, MySQL, and SQLite where unique constraints provide the atomicity guarantee.
- Rotation atomicity via unique-constraint inserts
- Revocation persistence in relational schemas
- Periodic cleanup of expired rows
MongoDB repository
DatabaseDocument backend using multi-document transactions (or duplicate-key detection) for atomic rotation.
- Transactional rotation marks with duplicate-key handling at the transaction boundary
- Optional transaction-free mode for standalone servers
- TTL-index-style cleanup of expired documents
In-memory repository
DatabaseMutex-guarded map for development, testing, and single-instance deployments — zero external dependencies.
- Mutex-based compare-and-swap rotation
- Interval-driven purge of expired entries
- Drop-in parity with the persistent backends for tests
Challenges & Solutions
Problems solved and lessons learned
Making refresh rotation race-free on four very different stores
TechnicalIf two requests rotate the same refresh token simultaneously (a retry, or an attacker replaying a stolen token), both could succeed and mint two valid token chains. Each storage backend has completely different atomicity primitives, so a naive check-then-write is racy on all of them.
Solution
Defined a single compare-and-swap contract — MarkTokenRotatedAtomic — and implemented it natively per backend: SETNX on Redis, multi-document transactions with duplicate-key detection on MongoDB, ON CONFLICT DO NOTHING inserts on GORM, and a mutex-guarded map in memory.
Outcome
A concurrency test fires 10 simultaneous rotations of one token against every backend; exactly one wins and nine fail with ErrTokenRotated, on all four implementations.
The rotation ordering bug that could lock users out forever
TechnicalRotateRefreshToken originally marked the old token as rotated before creating the replacement. If creation then failed (network blip, signing error), the old token was already dead — the user had no valid refresh token left and was permanently logged out.
Solution
Reordered the flow to create and persist the new token first, and only then mark the old one rotated. A failure now leaves the old token intact and the operation safely retryable.
Outcome
Fixed in v2.0.0 with regression tests; a mid-rotation failure is now recoverable instead of a forced re-login.
MongoDB duplicate-key handling inside vs. outside the transaction
TechnicalThe Mongo repository detected duplicate-key errors (the 'someone else already rotated this token' signal) inside the transaction callback. Mid-transaction write errors can cause the server to abort the transaction regardless of what the callback returns, making commit behavior dependent on driver version.
Solution
Moved duplicate-key detection out of the callback to the transaction boundary, classifying the error after the transaction settles instead of racing the server's abort logic.
Outcome
Rotation conflicts on MongoDB now resolve deterministically across driver versions.
Goroutine lifecycles that matched the documentation
TechnicalDoc comments claimed cleanup goroutines were stopped by GC finalization — they weren't, and Go doesn't work that way. Long-lived processes creating multiple makers would leak tickers, and Redis Close() panicked on a second call.
Solution
Added an explicit GourdianTokenMakerCloser interface with a context-driven, sync.Once-guarded idempotent Close(), kept separate from the main interface so existing implementers don't break, and fixed the lying documentation.
Outcome
Deterministic shutdown verified by tests, including a fix for a flaky race between context cancellation and a ticker tick.
Key Learnings
API design and reliability lessons from sixteen months and a major version
Don't force your type opinions on callers
Technicalv1 required uuid.UUID for user and session identifiers, forcing every consumer to depend on google/uuid and convert their own ID formats.
uuid.UUID everywhere in the public API; callers with string IDs, integer IDs, or composite keys had to shoehorn them into UUIDs.
v2 treats identifiers as opaque strings. Any non-empty userID works, google/uuid became an internal detail, and the accepted input space widened instead of narrowing.
Public APIs should constrain only what they actually rely on — everything else should be opaque to keep integration friction low.
Ordering is a correctness feature in multi-step state changes
TechnicalThe rotation lockout bug came from destroying old state before the new state existed.
Mark old token rotated → create new token → hope nothing fails in between.
Create new token first → then invalidate the old one; failures leave a recoverable state.
In any invalidate-and-replace flow, make the replacement exist before the invalidation — the same rule as writing the new file before deleting the old one.
Big-bang constructors don't scale — and doc comments must not lie
ProcessAn 18-parameter config constructor and a false claim about GC-driven goroutine cleanup both looked fine in code review and both aged badly.
NewGourdianTokenConfig with 18 positional parameters; docs asserting finalizers stop background goroutines.
Struct-literal configuration with a Default factory (constructor kept but deprecated), an explicit Close() contract, and documentation corrected to match reality.
Treat doc comments as testable claims, and prefer struct literals plus small factories over wide positional constructors.
Key Takeaways
- •One storage contract with per-backend native atomicity beats a lowest-common-denominator locking scheme.
- •Widening an API (UUID → opaque string) can be the breaking change that makes everything else simpler.
- •Create the replacement before invalidating the original — always.
- •Sentinel errors added alongside unchanged message text give errors.Is support without breaking string-matching callers.
Roadmap
Known gaps tracked in the v2.0.0 changelog
Planned
Algorithm re-check in revoke/rotate key funcs
The revoke and rotate paths parse tokens without re-validating the signing algorithm the way the verify path does. Low severity (verify still gates usage), but the paths should be symmetric.
Unify the MongoDB factory signature
NewGourdianTokenMakerWithMongo takes an extra positional transactionsEnabled bool that the other factories don't — fold it into the options pattern in the next major.
Unify Close() signatures across repositories
MongoTokenRepository.Close(ctx) takes a context while the other three backends expose a bare Close() — align them behind one contract.
Ideas & Backlog
Refresh benchmark suite on the current toolchain
LowPublished numbers were measured before the Go 1.24 toolchain bump — re-run and republish the full benchmark tables.
AI-readable content
Learn more →This content is available via the AI Content API as JSON or token-efficient Markdown. Feed it directly into LLM workflows.
/api/content/projects/gourdiantokenRelated Content
Explore related articles, projects, and tools.