Documentation
ยถ
Overview ยถ
Package gourdiantoken provides a production-ready JWT token management system with comprehensive security features, flexible storage backends, and support for multiple cryptographic algorithms.
Overview ยถ
The gourdiantoken package implements a robust JWT token lifecycle management system designed for enterprise authentication systems. It provides:
- Access and refresh token generation with configurable lifetimes
- Cryptographic signing with symmetric (HMAC) and asymmetric (RSA, ECDSA, EdDSA) algorithms
- Token verification with comprehensive security checks
- Optional token revocation for logout and security incident response
- Refresh token rotation for preventing token replay attacks
- Multiple storage backends for revocation/rotation tracking
- Thread-safe concurrent operations with context-aware cancellation
- Automatic background cleanup of expired tokens
- Highly configurable security policies
Architecture ยถ
## Core Components
### Token Types
The package supports two primary token types:
AccessToken: Short-lived tokens (default 30 minutes) containing user identity, session information, and authorization roles. Used for API authorization and should be transmitted in Authorization headers. AccessTokenClaims include the "rls" (roles) claim for RBAC.
RefreshToken: Long-lived tokens (default 7 days) used solely to obtain new access tokens without re-authentication. RefreshTokenClaims do not include roles since they're not directly used for authorization. Should be stored securely (httpOnly cookies recommended).
### Cryptographic Support
Symmetric algorithms (HMAC):
- HS256: HMAC with SHA-256 (recommended for development)
- HS384: HMAC with SHA-384
- HS512: HMAC with SHA-512
Asymmetric algorithms (RSA with PKCS#1 v1.5):
- RS256, RS384, RS512: RSA signatures with PKCS#1 v1.5
Asymmetric algorithms (RSA-PSS, recommended for new implementations):
- PS256, PS384, PS512: RSA PSS signatures
ECDSA algorithms:
- ES256: ECDSA with P-256 curve and SHA-256
- ES384: ECDSA with P-384 curve and SHA-384
- ES512: ECDSA with P-521 curve and SHA-512
EdDSA algorithms:
- EdDSA: Ed25519 signatures (modern, recommended for new systems)
### Storage Backends
The package provides multiple implementations of TokenRepository for different deployment scenarios:
MemoryTokenRepository: In-memory storage with background cleanup. Suitable for development, testing, and single-instance deployments. Data lost on restart.
GormTokenRepository: SQL database support via GORM ORM. Works with PostgreSQL, MySQL, SQLite, SQL Server, and CockroachDB. Recommended for production systems with persistent storage requirements.
MongoTokenRepository: MongoDB document storage with optional transaction support. Provides automatic TTL index-based cleanup and horizontal scaling via sharding.
RedisTokenRepository: High-performance in-memory store with TTL-based expiration. Recommended for systems requiring sub-millisecond token validation latency. Supports Redis Cluster for distributed deployments.
### Configuration
All token creation, validation, and security policies are controlled through GourdianTokenConfig. Two factory methods provide different levels of control:
DefaultGourdianTokenConfig(): Pre-configured with secure HMAC defaults suitable for immediate use. Customizable after creation.
NewGourdianTokenConfig(): Full explicit control for complex requirements or asymmetric algorithm setup.
## Token Claims Structure
### AccessTokenClaims (JWT Payload)
Standard JWT claims (RFC 7519):
- jti (JWT ID): Unique token identifier (UUIDv4)
- iss (Issuer): Authentication service identifier
- aud (Audience): Intended recipients list
- sub (Subject): User's unique identifier (UUID)
- iat (Issued At): Token creation timestamp
- exp (Expiration Time): Token expiration timestamp
- nbf (Not Before): Optional token validity start time
- typ (Type): Always "access" for access tokens
Custom claims:
- sid (Session ID): Session identifier for tracking user sessions
- usr (Username): Human-readable username for logging
- rls (Roles): Array of authorization roles for RBAC
- mle (Maximum Lifetime Expiry): Absolute expiration regardless of refreshes
### RefreshTokenClaims (JWT Payload)
Similar structure to AccessTokenClaims but without:
- rls (Roles): Not included in refresh tokens
Both token types include all required JWT claims and custom tracking information.
Key Features and Behaviors ยถ
## Token Creation
Access Token Flow:
- Validates input parameters (user ID, roles, session ID)
- Generates unique token ID (UUIDv4)
- Sets timestamps: iat (now), exp (now + AccessExpiryDuration), nbf (now), mle (now + AccessMaxLifetimeExpiry)
- Cryptographically signs the token payload
- Returns AccessTokenResponse with signed JWT and metadata
Refresh Token Flow:
- Similar validation and setup as access tokens
- No roles included in token
- Longer default lifetime for session continuity
- Returns RefreshTokenResponse with signed JWT and metadata
## Token Verification
Access Token Verification Process:
- Check context cancellation
- Check revocation status (if RevocationEnabled)
- Verify cryptographic signature
- Validate algorithm matches expected method
- Check all timestamps: iat, exp, nbf, mle
- Verify required claims presence
- Validate token type is "access"
- Parse and return AccessTokenClaims
Refresh Token Verification Process:
- Same as access token verification, plus:
- Check rotation status (if RotationEnabled)
- Return RefreshTokenClaims
Verification failures provide detailed error information for debugging and logging.
## Token Revocation
Revocation operates as follows:
- User initiates logout or revocation request
- RevokeAccessToken() / RevokeRefreshToken() is called
- Token is parsed to extract expiration time
- Token hash (SHA-256) is stored in repository with TTL
- TTL is set to match token's natural expiration time
- Subsequent verification checks revocation status before accepting token
- Background cleanup automatically removes expired revocation records
Use cases:
- User logout (revoke both access and refresh tokens)
- Security incident response (immediately revoke compromised tokens)
- Password change (revoke all user tokens)
- Session termination (revoke all session tokens)
## Token Rotation (Refresh Token Rotation)
Rotation prevents refresh token reuse and provides attack detection:
- Client calls RotateRefreshToken() with old refresh token
- Old token is verified (signature, expiration, revocation status)
- Old token is atomically marked as rotated using compare-and-swap
- If already rotated by another request, operation fails (attack detected)
- New refresh token is created with fresh expiration time
- New token is returned to client; old token is now invalid
- Background cleanup removes expired rotation records
Security guarantees:
- Only one concurrent rotation succeeds (atomic operation)
- Multiple attempts detect possible token theft
- Limits blast radius of compromised tokens
- Enables enforcing re-authentication after detecting reuse
Rotation detection enables strong security policies:
- Log potential security incidents
- Force re-authentication for suspicious patterns
- Invalidate entire sessions
- Trigger additional security checks (MFA, device verification)
Configuration Guide ยถ
## GourdianTokenConfig Fields
Signing and Algorithm Configuration:
- SigningMethod: Must be either Symmetric or Asymmetric
- Algorithm: Specific JWT algorithm (HS256, RS256, ES256, EdDSA, etc.)
- SymmetricKey: Base64-encoded secret for HMAC algorithms (required for Symmetric)
- PrivateKeyPath: Path to PEM-encoded private key (required for Asymmetric)
- PublicKeyPath: Path to PEM-encoded public key (required for Asymmetric)
JWT Claims Configuration:
- Issuer: Authentication service identifier (included in "iss" claim)
- Audience: Intended token recipients (included in "aud" claim)
- AllowedAlgorithms: Whitelist of acceptable algorithms during verification
- RequiredClaims: List of mandatory claims beyond JWT standard claims
Token Lifetime Configuration:
- AccessExpiryDuration: Time until access token expires after issuance (default 30m)
- AccessMaxLifetimeExpiry: Absolute maximum validity period for access tokens (default 24h)
- RefreshExpiryDuration: Time until refresh token expires after issuance (default 7d)
- RefreshMaxLifetimeExpiry: Absolute maximum validity period for refresh tokens (default 30d)
- RefreshReuseInterval: Minimum time between token reuse attempts (default 5m)
Feature Flags and Cleanup:
- RotationEnabled: Whether to enforce refresh token rotation (default false)
- RevocationEnabled: Whether to allow token revocation (default false)
- CleanupInterval: How often to remove expired tokens from storage (default 6h)
## Configuration Examples
Symmetric (HMAC) Development Configuration:
config := gourdiantoken.DefaultGourdianTokenConfig(
"your-secret-key-at-least-32-bytes-long",
)
config.Issuer = "auth.example.com"
config.Audience = []string{"api.example.com"}
config.AccessExpiryDuration = 15 * time.Minute
config.RefreshExpiryDuration = 24 * time.Hour
Asymmetric (RSA) Production Configuration:
config := gourdiantoken.NewGourdianTokenConfig(
gourdiantoken.Asymmetric,
true, true, // rotation, revocation enabled
[]string{"api.example.com"},
[]string{"RS256", "PS256"},
[]string{"iss", "aud", "nbf", "mle"},
"RS256",
"", // no symmetric key for asymmetric
"/secure/private.pem",
"/secure/public.pem",
"auth.production.example.com",
15*time.Minute, 24*time.Hour,
7*24*time.Hour, 30*24*time.Hour,
5*time.Minute, 6*time.Hour,
)
EdDSA (Ed25519) Production Configuration:
config := gourdiantoken.NewGourdianTokenConfig(
gourdiantoken.Asymmetric,
true, true,
[]string{"api.example.com"},
[]string{"EdDSA"},
[]string{"iss", "aud", "nbf", "mle"},
"EdDSA",
"",
"/keys/ed25519-private.pem",
"/keys/ed25519-public.pem",
"auth.example.com",
15*time.Minute, 24*time.Hour,
7*24*time.Hour, 30*24*time.Hour,
5*time.Minute, 6*time.Hour,
)
Factory Methods and Initialization ยถ
## Stateless Token Maker (No Storage)
For systems that only validate tokens without revocation/rotation:
maker, err := gourdiantoken.NewGourdianTokenMakerNoStorage(ctx, config)
if err != nil {
log.Fatal(err)
}
Use cases:
- Microservices that only validate tokens
- Distributed systems with no shared state
- High-performance scenarios where database lookups unacceptable
Limitations:
- RevocationEnabled must be false
- RotationEnabled must be false
- No token revocation capability
- No refresh token rotation
## In-Memory Token Maker
For development and testing with optional revocation/rotation:
maker, err := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config)
if err != nil {
log.Fatal(err)
}
Characteristics:
- Automatic background cleanup goroutine
- Data lost on application restart
- Sub-microsecond token operations
- Suitable for single-instance deployments
- Perfect for testing authentication logic
## SQL Database Token Maker (GORM)
For production systems with SQL databases:
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithGorm(ctx, config, gormDB)
if err != nil {
log.Fatal(err)
}
Supported databases:
- PostgreSQL (recommended for production)
- MySQL/MariaDB
- SQLite (development only)
- SQL Server
- CockroachDB
Features:
- ACID transaction support
- Complex query capabilities
- Connection pooling
- Automatic migrations
- Composite indexing for performance
## MongoDB Token Maker
For document-oriented production systems:
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
if err != nil {
log.Fatal(err)
}
mongoDB := client.Database("auth_service")
maker, err := gourdiantoken.NewGourdianTokenMakerWithMongo(ctx, config, mongoDB)
if err != nil {
log.Fatal(err)
}
Features:
- Automatic TTL-based cleanup via indexes
- Optional transaction support
- Horizontal scaling via sharding
- High-write throughput
- Flexible document schema
Transaction behavior:
- Pass true to enable transactions (requires replica set)
- Pass false for standalone instances
## Redis Token Maker
For high-performance systems requiring sub-millisecond validation:
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 100,
})
maker, err := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient)
if err != nil {
log.Fatal(err)
}
Performance characteristics:
- Sub-microsecond token operations
- O(1) for all operations
- Built-in TTL-based expiration
- Distributed via Redis Cluster
- High availability via Sentinel
Basic Usage Patterns ยถ
## Complete Authentication Flow
// 1. Initialize token maker
config := gourdiantoken.DefaultGourdianTokenConfig("secret-key")
maker, err := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config)
if err != nil {
return err
}
defer maker.Close()
// 2. Login endpoint - create token pair
accessToken, err := maker.CreateAccessToken(
ctx,
userID, // uuid.UUID
email, // string
[]string{"user"}, // roles
sessionID, // uuid.UUID
)
if err != nil {
return err
}
refreshToken, err := maker.CreateRefreshToken(ctx, userID, email, sessionID)
if err != nil {
return err
}
// 3. Return tokens to client
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"access_token": accessToken.Token,
"refresh_token": refreshToken.Token,
})
// 4. Middleware - verify access token
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := maker.VerifyAccessToken(r.Context(), token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// Attach claims to context
ctx := context.WithValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 5. Token refresh endpoint
refreshToken := r.Header.Get("X-Refresh-Token")
newRefresh, err := maker.RotateRefreshToken(ctx, refreshToken)
if err != nil {
http.Error(w, "invalid refresh token", http.StatusUnauthorized)
return
}
newAccess, err := maker.CreateAccessToken(
ctx, claims.Subject, claims.Username, roles, claims.SessionID)
if err != nil {
http.Error(w, "failed to create token", http.StatusInternalServerError)
return
}
// 6. Logout endpoint
accessToken := r.Header.Get("Authorization")
accessToken = strings.TrimPrefix(accessToken, "Bearer ")
maker.RevokeAccessToken(ctx, accessToken)
maker.RevokeRefreshToken(ctx, refreshTokenString)
## RBAC Authorization
func requireRole(requiredRole string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("claims").(*gourdiantoken.AccessTokenClaims)
hasRole := false
for _, role := range claims.Roles {
if role == requiredRole {
hasRole = true
break
}
}
if !hasRole {
http.Error(w, "insufficient permissions", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
Security Considerations ยถ
## Algorithm Selection
Symmetric (HMAC):
- Pros: Fastest performance, simplest deployment
- Cons: Shared secret must be distributed securely
- Recommendation: Development only, not production
- Minimum key size: 32 bytes (256 bits)
RSA/RSA-PSS:
- Pros: Public key can be distributed, widely supported
- Cons: Slower than ECDSA for signing
- Recommendation: Good for backward compatibility
- Minimum key size: 2048 bits (3072+ recommended)
ECDSA:
- Pros: Efficient key sizes, good performance
- Cons: Implementation complexity
- Recommendation: Excellent general-purpose choice
- Recommended curves: P-256, P-384
EdDSA:
- Pros: Modern, fast, resistant to side-channel attacks
- Cons: Less widely supported in legacy systems
- Recommendation: Best for new implementations
- Standard algorithm: Ed25519 (128-bit security)
## Key Management Best Practices
For Symmetric Keys:
- Generate using cryptographically secure random source
- Store in secure configuration management (Vault, etc.)
- Never commit to version control
- Rotate periodically (every 90 days recommended)
- Use environment variables or secrets managers
For Asymmetric Keys:
- Generate using proper key generation tools
- Store private keys with 0600 file permissions
- Store public keys separately for distribution
- Use PKCS#8 or SEC1 formats (PEM-encoded)
- Implement key rotation strategy
- Consider hardware security modules for sensitive systems
## Token Lifetime Recommendations
Access Tokens:
- Short-lived: 15-30 minutes for high-security systems
- Medium-lived: 30-60 minutes for balanced security
- Minimum: 5 minutes (usability threshold)
- Maximum: 2 hours (beyond this, use refresh tokens)
Refresh Tokens:
- Short-lived: 1-7 days for high-security systems
- Medium-lived: 1-4 weeks for balanced security
- Long-lived: 30-90 days for consumer apps
- Maximum lifetime should limit implicit token chains
## Security Features Configuration
For Development:
config := gourdiantoken.DefaultGourdianTokenConfig("dev-secret-key")
config.RotationEnabled = false
config.RevocationEnabled = false
config.AccessExpiryDuration = 1 * time.Hour
For Production:
config := gourdiantoken.NewGourdianTokenConfig(
gourdiantoken.Asymmetric,
true, true, // Enable rotation and revocation
[]string{"api.example.com"},
[]string{"RS256", "ES256", "EdDSA"},
[]string{"iss", "aud", "nbf", "mle"},
"ES256",
"",
"/secure/private.pem",
"/secure/public.pem",
"auth.example.com",
15*time.Minute, 24*time.Hour,
7*24*time.Hour, 30*24*time.Hour,
5*time.Minute, 6*time.Hour,
)
## Attack Prevention
Token Replay Attack Prevention:
- Enable RotationEnabled for refresh token protection
- Implement RefreshReuseInterval for delay-based detection
- Monitor for multiple rotation attempts (indicates theft)
- Store refresh tokens securely (httpOnly cookies)
Token Theft Mitigation:
- Enable RevocationEnabled for logout support
- Implement device fingerprinting
- Use short access token lifetimes
- Implement rate limiting on token endpoints
- Monitor for suspicious token patterns
Key Compromise Response:
- Revoke all existing tokens immediately
- Force full re-authentication
- Rotate compromised keys
- Audit token usage logs
- Consider additional verification (MFA)
Storage Backend Comparison ยถ
## Selection Matrix
Use MemoryTokenRepository when:
- Developing and testing
- Single-instance deployment
- Restart-based token reset acceptable
- Testing authentication logic
Use GormTokenRepository when:
- Existing SQL database in use
- ACID transaction requirements critical
- Complex query patterns needed
- Multi-instance deployment with shared database
Use MongoTokenRepository when:
- Document-oriented storage preferred
- Horizontal scaling required
- Flexible schema beneficial
- TTL-based cleanup sufficient
Use RedisTokenRepository when:
- Sub-millisecond latency critical
- High token validation throughput
- Can accept in-memory storage
- Distributed via cluster or Sentinel
## Performance Characteristics
Operation Latencies (approximate):
- Memory: 1-10 microseconds
- Redis: 50-500 microseconds (network dependent)
- MongoDB: 500-5000 microseconds (with indexes)
- SQL (GORM): 1-10 milliseconds (with indexes)
Token Creation Performance:
- Symmetric signing: 100,000+ tokens/second
- ECDSA signing: 50,000+ tokens/second
- RSA signing: 1,000-5,000 tokens/second
- EdDSA signing: 50,000+ tokens/second
Token Verification Performance:
- Symmetric verification: 500,000+ tokens/second
- ECDSA verification: 200,000+ tokens/second
- RSA verification: 50,000+ tokens/second (much faster than signing)
- EdDSA verification: 200,000+ tokens/second
Error Handling ยถ
## Common Error Scenarios
Invalid Token Errors:
- Malformed JWT: "invalid token" with parsing details
- Signature mismatch: "invalid token" with algorithm details
- Expired token: "token has expired"
- Future token: "token issued in the future"
- Exceeds max lifetime: "token exceeded maximum lifetime"
Revocation Errors:
- Revocation disabled: "access token revocation is not enabled"
- Token revoked: "token has been revoked"
- Database error: detailed error with context
Rotation Errors:
- Rotation disabled: "token rotation not enabled"
- Already rotated: "token has been rotated"
- Database error: detailed error with context
Configuration Errors:
- Algorithm mismatch: "algorithm HS256 not compatible with asymmetric signing"
- Key file missing: "failed to read private key file"
- Invalid duration: "access token duration must be positive"
- Invalid key size: "symmetric key must be at least 32 bytes"
## Error Recovery Patterns
// Handle token expiration
claims, err := maker.VerifyAccessToken(ctx, token)
if err != nil {
if strings.Contains(err.Error(), "expired") {
// Attempt refresh
newToken, err := refreshAccessToken(ctx)
if err != nil {
// Force re-authentication
return redirectToLogin()
}
return newToken
}
return handleAuthError(err)
}
// Handle rotation detection
newToken, err := maker.RotateRefreshToken(ctx, oldToken)
if err != nil {
if strings.Contains(err.Error(), "rotated") {
// Suspicious activity detected
logSecurityAlert("token reuse detected")
revokeAllUserTokens(ctx, userID)
return errors.New("security violation: session terminated")
}
return handleError(err)
}
Advanced Topics ยถ
## Multi-Service Token Validation
For distributed systems where multiple services validate the same tokens:
// Auth service (creates tokens) authMaker, _ := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient) // API service (validates tokens) apiMaker, _ := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient)
Both services use same Redis backend for revocation/rotation checking. For stateless validation without storage, omit the repository.
## Custom Claims Validation
config.RequiredClaims = []string{"iss", "aud", "nbf", "mle", "custom_claim"}
config.Audience = []string{"service1", "service2"}
Claims are validated during verification. Additional custom logic can be applied after verification:
claims, err := maker.VerifyAccessToken(ctx, token)
if err != nil {
return err
}
if !slices.Contains(claims.Roles, "admin") {
return errors.New("admin role required")
}
## Token Metadata and Auditing
All token operations include comprehensive metadata:
accessResp, _ := maker.CreateAccessToken(ctx, userID, username, roles, sessionID) // Contains: IssuedAt, ExpiresAt, NotBefore, MaxLifetimeExpiry // Use for audit logs and client display claims, _ := maker.VerifyAccessToken(ctx, token) // Contains: ID (jti), Subject, SessionID for detailed tracking // Use for request logging and security audit trails
## Background Cleanup Management
Cleanup goroutines run automatically for token makers with storage:
maker, _ := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config) // Cleanup runs automatically at config.CleanupInterval // Graceful shutdown maker.Close() // Stops cleanup goroutine
Cleanup behavior:
- Runs at configured intervals
- Removes expired revocation records
- Removes expired rotation records
- Continues automatically until stopped
- Can be called manually for testing
Testing Patterns ยถ
## Unit Testing
func TestAccessTokenCreation(t *testing.T) {
maker := createTestMaker()
userID := uuid.New()
sessionID := uuid.New()
token, err := maker.CreateAccessToken(
context.Background(),
userID,
"testuser",
[]string{"user"},
sessionID,
)
require.NoError(t, err)
assert.NotEmpty(t, token.Token)
assert.Equal(t, userID, token.Subject)
}
## Integration Testing
func TestTokenRotation(t *testing.T) {
redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
config := gourdiantoken.DefaultGourdianTokenConfig("test-secret")
config.RotationEnabled = true
maker, _ := gourdiantoken.NewGourdianTokenMakerWithRedis(
context.Background(), config, redisClient)
// Create refresh token
refresh, _ := maker.CreateRefreshToken(context.Background(), userID, "user", sessionID)
// Rotate token
newRefresh, err := maker.RotateRefreshToken(context.Background(), refresh.Token)
require.NoError(t, err)
assert.NotEqual(t, refresh.Token, newRefresh.Token)
// Old token should be invalid
_, err = maker.VerifyRefreshToken(context.Background(), refresh.Token)
assert.Error(t, err)
}
## Benchmark Testing
func BenchmarkAccessTokenCreation(b *testing.B) {
maker := createBenchMaker()
userID := uuid.New()
sessionID := uuid.New()
b.ResetTimer()
for i := 0; i < b.N; i++ {
maker.CreateAccessToken(context.Background(), userID, "user", []string{"role"}, sessionID)
}
}
func BenchmarkAccessTokenVerification(b *testing.B) {
maker := createBenchMaker()
token, _ := maker.CreateAccessToken(context.Background(), userID, "user", []string{"role"}, sessionID)
b.ResetTimer()
for i := 0; i < b.N; i++ {
maker.VerifyAccessToken(context.Background(), token.Token)
}
}
Thread Safety and Concurrency ยถ
All methods in GourdianTokenMaker are thread-safe for concurrent use across multiple goroutines. The implementation uses:
- Immutable state after initialization (no race conditions)
- Thread-safe underlying JWT library
- Concurrent-safe repository implementations
- Proper context handling for cancellation
Safe concurrent usage:
for i := 0; i < 100; i++ {
go func() {
claims, err := maker.VerifyAccessToken(ctx, token)
// Handle result
}()
}
MemoryTokenRepository uses RWMutex for concurrent access:
- Multiple concurrent readers (VerifyAccessToken)
- Exclusive writers (RevokeAccessToken, RotateRefreshToken)
- Automatic background cleanup without blocking operations
Redis and database repositories provide inherent thread safety through their underlying implementations.
Context Handling ยถ
All operations accept a context.Context for cancellation, timeout, and deadline management:
// With timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() claims, err := maker.VerifyAccessToken(ctx, token) // With cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() token, err := maker.CreateAccessToken(ctx, userID, username, roles, sessionID) // Operations check context at multiple points // Cancellation returns immediately with context error
Dependencies ยถ
Required external dependencies:
- github.com/golang-jwt/jwt/v5: JWT implementation
- github.com/google/uuid: UUID generation and parsing
Optional dependencies (for storage backends):
- gorm.io/gorm: SQL database ORM
- go.mongodb.org/mongo-driver: MongoDB database driver
- github.com/redis/go-redis/v9: Redis client
All dependencies use modern versions and stable APIs.
Version Compatibility ยถ
Minimum requirements:
- Go 1.18 or later (generics support)
- JWT library v5.x
- UUID library v1.x
Database backend version requirements:
- GORM: v1.25+
- MongoDB: 4.0+ (4.2+ recommended for transactions)
- Redis: 6.0+ (recommended)
Migration and Upgrade Guide ยถ
## From Stateless to Revocation-Enabled
Existing deployments can add revocation without recreating tokens:
// Step 1: Choose storage backend (e.g., Redis)
redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
// Step 2: Update configuration
config.RevocationEnabled = true
config.RotationEnabled = true
// Step 3: Create new maker with storage
maker, err := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient)
// Existing tokens remain valid until natural expiration
// New tokens support revocation immediately
## Algorithm Migration
Transitioning from HMAC to RSA:
// Phase 1: Both algorithms allowed
config.AllowedAlgorithms = []string{"HS256", "RS256"}
// Phase 2: Create tokens with new algorithm
config.Algorithm = "RS256"
// Old HS256 tokens still validated
// Phase 3: After expiration period, restrict to RS256
config.AllowedAlgorithms = []string{"RS256"}
## Storage Backend Migration
From memory to Redis:
// Existing revoked/rotated tokens are lost (acceptable for transition) oldMaker, _ := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config) oldMaker.Close() // Deploy new Redis-backed maker redisClient := redis.NewClient(...) newMaker, _ := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient)
Common Pitfalls and Solutions ยถ
## Pitfall: Token Expiration Not Enforced
Problem: All tokens are accepted regardless of expiration time. Solution: Ensure RequiredClaims includes "exp" and token verification is called.
// Correct: Always call VerifyAccessToken
claims, err := maker.VerifyAccessToken(ctx, token)
if err != nil {
return errors.New("token validation failed")
}
// Wrong: Parsing JWT without verification
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return "any-secret", nil
})
// This bypasses all security checks
## Pitfall: Symmetric Key Too Short
Problem: Configuration validation passes but security is weak. Solution: Use cryptographically secure random 32+ byte keys.
// Correct
key := make([]byte, 32)
rand.Read(key)
encodedKey := base64.RawURLEncoding.EncodeToString(key)
config := gourdiantoken.DefaultGourdianTokenConfig(encodedKey)
// Wrong
config := gourdiantoken.DefaultGourdianTokenConfig("short-key")
// Fails validation
## Pitfall: Refresh Token Rotation Without Storage
Problem: Configuration requires storage but none provided. Solution: Provide repository or disable rotation.
// Correct: Use with storage config.RotationEnabled = true maker, err := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config) // Correct: Disable rotation for stateless config.RotationEnabled = false maker, err := gourdiantoken.NewGourdianTokenMakerNoStorage(ctx, config)
## Pitfall: Revocation Checks Not Running
Problem: Revoked tokens still validate successfully. Solution: Ensure RevocationEnabled is true and storage is provided.
config.RevocationEnabled = true maker, err := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient) // RevocationEnabled must be true AND valid repository provided
## Pitfall: Private Key File Permissions Too Permissive
Problem: Security validation rejects private key during initialization. Solution: Set proper file permissions before creating maker.
// Correct chmod 0600 /path/to/private.pem // Then create maker maker, err := gourdiantoken.NewGourdianTokenMaker(ctx, config, repo)
Performance Tuning ยถ
## Token Creation Optimization
// Batch token creation for high throughput
tokens := make([]*gourdiantoken.AccessTokenResponse, 1000)
for i := 0; i < 1000; i++ {
token, _ := maker.CreateAccessToken(ctx, userID, username, roles, sessionID)
tokens[i] = token
}
// Use context with timeout for batch operations
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
## Verification Caching (Application Level)
For high-frequency verification of same tokens:
var cache sync.Map // Simple in-memory cache
func verifyWithCache(token string) (*Claims, error) {
if cached, ok := cache.Load(token); ok {
return cached.(*Claims), nil
}
claims, err := maker.VerifyAccessToken(ctx, token)
if err != nil {
return nil, err
}
cache.Store(token, claims)
return claims, nil
}
## Redis Connection Pool Tuning
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 100, // Adjust based on concurrency
MinIdleConns: 10, // Keep minimum connections open
})
Monitoring and Observability ยถ
## Token Statistics
Most repository implementations provide Stats() for monitoring:
stats, err := repo.Stats(ctx)
// Returns counts of revoked/rotated tokens
// Use for Prometheus metrics or logging
log.Printf("Active revocations: %d", stats["total_revoked_tokens"])
log.Printf("Rotated tokens: %d", stats["rotated_tokens"])
## Audit Logging
func auditLog(action string, userID uuid.UUID, result error) {
log.Printf("[AUDIT] action=%s user=%s result=%v timestamp=%s",
action, userID, result, time.Now())
}
// Usage
auditLog("token_created", userID, nil)
auditLog("token_verified", userID, nil)
auditLog("token_revoked", userID, nil)
## Health Checks
func healthCheck(maker gourdiantoken.GourdianTokenMaker) error {
// Create and verify test token
token, err := maker.CreateAccessToken(
context.Background(),
testUserID,
"health-check",
[]string{"test"},
testSessionID,
)
if err != nil {
return err
}
_, err = maker.VerifyAccessToken(context.Background(), token.Token)
return err
}
Related Patterns and Best Practices ยถ
## Token-Based Session Management
Using tokens for session tracking:
// Create session entry with tokens
session := &Session{
ID: sessionID,
UserID: userID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
}
db.SaveSession(session)
// Verify session still active during token verification
claims, _ := maker.VerifyAccessToken(ctx, token)
session, _ := db.GetSession(claims.SessionID)
if session == nil || session.ExpiresAt.Before(time.Now()) {
return errors.New("session expired")
}
## Multi-Tenant Token Isolation
For multi-tenant systems, include tenant information in token:
// Custom implementation using token claims
type TenantAccessClaims struct {
*gourdiantoken.AccessTokenClaims
TenantID string `json:"tid"`
}
// Verify tenant matches request
claims, _ := maker.VerifyAccessToken(ctx, token)
if claims.Subject != requestUserID {
return errors.New("user mismatch")
}
## Device Fingerprinting
For enhanced security, validate device consistency:
// Create token with device fingerprint
deviceID := hashDeviceInfo(r.Header)
// Store in session/database
// Verify device matches during token validation
currentDevice := hashDeviceInfo(r.Header)
if currentDevice != storedDevice {
// Suspicious activity - require re-authentication
}
License and Support ยถ
This package is provided as-is for production use. For issues, feature requests, or contributions, refer to the repository's issue tracker and contribution guidelines.
Conclusion ยถ
The gourdiantoken package provides a comprehensive, flexible, and secure JWT token management system suitable for small startups to large enterprises. Its modular design with multiple storage backends and cryptographic algorithms allows customization for virtually any authentication requirement while maintaining strict security standards through validation, configuration enforcement, and best-practice defaults.
Package gourdiantoken provides a comprehensive JWT token management system with support for access and refresh tokens, token rotation, revocation, and multiple signing algorithms.
Features:
- Symmetric (HMAC) and Asymmetric (RSA, ECDSA, EdDSA) signing methods
- Token rotation with atomic operations to prevent race conditions
- Token revocation with background cleanup
- Configurable expiry durations and maximum lifetimes
- Context-aware operations for cancellation support
- Comprehensive validation and security checks
Index ยถ
- Variables
- type AccessTokenClaims
- type AccessTokenResponse
- type BenchRepositoryFactory
- type GormTokenRepository
- func (r *GormTokenRepository) CleanupAll(ctx context.Context) error
- func (r *GormTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
- func (r *GormTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
- func (r *GormTokenRepository) Close() error
- func (r *GormTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
- func (r *GormTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
- func (r *GormTokenRepository) IsTokenRotated(ctx context.Context, token string) (bool, error)
- func (r *GormTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
- func (r *GormTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
- func (r *GormTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
- func (r *GormTokenRepository) Stats(ctx context.Context) (map[string]interface{}, error)
- type GourdianTokenConfig
- type GourdianTokenMaker
- func DefaultGourdianTokenMaker(ctx context.Context, symmetricKey string, tokenRepo TokenRepository) (GourdianTokenMaker, error)
- func NewGourdianTokenMaker(ctx context.Context, config GourdianTokenConfig, tokenRepo TokenRepository) (GourdianTokenMaker, error)
- func NewGourdianTokenMakerNoStorage(ctx context.Context, config GourdianTokenConfig) (GourdianTokenMaker, error)
- func NewGourdianTokenMakerWithGorm(ctx context.Context, config GourdianTokenConfig, db *gorm.DB) (GourdianTokenMaker, error)
- func NewGourdianTokenMakerWithMemory(ctx context.Context, config GourdianTokenConfig) (GourdianTokenMaker, error)
- func NewGourdianTokenMakerWithMongo(ctx context.Context, config GourdianTokenConfig, mongoDB *mongo.Database, ...) (GourdianTokenMaker, error)
- func NewGourdianTokenMakerWithRedis(ctx context.Context, config GourdianTokenConfig, redisClient *redis.Client) (GourdianTokenMaker, error)
- type JWTMaker
- func (maker *JWTMaker) CreateAccessToken(ctx context.Context, userID uuid.UUID, username string, roles []string, ...) (*AccessTokenResponse, error)
- func (maker *JWTMaker) CreateRefreshToken(ctx context.Context, userID uuid.UUID, username string, sessionID uuid.UUID) (*RefreshTokenResponse, error)
- func (maker *JWTMaker) RevokeAccessToken(ctx context.Context, token string) error
- func (maker *JWTMaker) RevokeRefreshToken(ctx context.Context, token string) error
- func (maker *JWTMaker) RotateRefreshToken(ctx context.Context, oldToken string) (*RefreshTokenResponse, error)
- func (maker *JWTMaker) VerifyAccessToken(ctx context.Context, tokenString string) (*AccessTokenClaims, error)
- func (maker *JWTMaker) VerifyRefreshToken(ctx context.Context, tokenString string) (*RefreshTokenClaims, error)
- type MemoryTokenRepository
- func (m *MemoryTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
- func (m *MemoryTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
- func (m *MemoryTokenRepository) Close() error
- func (m *MemoryTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
- func (m *MemoryTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
- func (m *MemoryTokenRepository) IsTokenRotated(ctx context.Context, token string) (bool, error)
- func (m *MemoryTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
- func (m *MemoryTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
- func (m *MemoryTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
- func (m *MemoryTokenRepository) Stats() map[string]int
- type MongoTokenRepository
- func (r *MongoTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
- func (r *MongoTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
- func (r *MongoTokenRepository) Close(ctx context.Context) error
- func (r *MongoTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
- func (r *MongoTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
- func (r *MongoTokenRepository) IsTokenRotated(ctx context.Context, token string) (bool, error)
- func (r *MongoTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
- func (r *MongoTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
- func (r *MongoTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
- func (r *MongoTokenRepository) Stats(ctx context.Context) (map[string]interface{}, error)
- type RedisTokenRepository
- func (r *RedisTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
- func (r *RedisTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
- func (r *RedisTokenRepository) Close() error
- func (r *RedisTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
- func (r *RedisTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
- func (r *RedisTokenRepository) IsTokenRotated(ctx context.Context, token string) (bool, error)
- func (r *RedisTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
- func (r *RedisTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
- func (r *RedisTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
- func (r *RedisTokenRepository) Stats(ctx context.Context) (map[string]interface{}, error)
- type RefreshTokenClaims
- type RefreshTokenResponse
- type RevokedTokenType
- type RotatedTokenType
- type SigningMethod
- type TestRepositoryFactory
- type TokenRepository
- func NewGormTokenRepository(db *gorm.DB) (TokenRepository, error)
- func NewMemoryTokenRepository(cleanupInterval time.Duration) TokenRepository
- func NewMongoTokenRepository(db *mongo.Database, useTransactions bool) (TokenRepository, error)
- func NewRedisTokenRepository(client *redis.Client) (TokenRepository, error)
- type TokenType
Constants ยถ
This section is empty.
Variables ยถ
var Version = "v1.0.7"
Functions ยถ
This section is empty.
Types ยถ
type AccessTokenClaims ยถ
type AccessTokenClaims struct {
// ID is the unique token identifier (UUIDv4) used for tracking and revocation.
ID uuid.UUID `json:"jti"`
// Subject is the user's unique identifier (UUID).
Subject uuid.UUID `json:"sub"`
// SessionID uniquely identifies the user's session (UUIDv4).
// Used to invalidate all tokens when a session ends.
SessionID uuid.UUID `json:"sid"`
// Username is the human-readable username for logging and display purposes.
Username string `json:"usr"`
// Issuer identifies the service that created this token (e.g., "auth.example.com").
Issuer string `json:"iss"`
// Audience lists the services that should accept this token (e.g., ["api.example.com"]).
Audience []string `json:"aud"`
// Roles contains the authorization roles for role-based access control (RBAC).
// Must contain at least one role.
Roles []string `json:"rls"`
// IssuedAt is the timestamp when this token was created (UTC).
IssuedAt time.Time `json:"iat"`
// ExpiresAt is the timestamp when this token expires (UTC).
ExpiresAt time.Time `json:"exp"`
// NotBefore is the optional timestamp before which the token is not valid (UTC).
NotBefore time.Time `json:"nbf"`
// MaxLifetimeExpiry is the absolute expiration time regardless of refreshes (RFC3339 format).
MaxLifetimeExpiry time.Time `json:"mle"`
// TokenType is always "access" for access tokens.
TokenType TokenType `json:"typ"`
}
AccessTokenClaims represents the claims contained in an access token JWT. Access tokens are short-lived and include authorization information (roles).
Standard JWT Claims:
- jti: JWT ID (unique token identifier)
- sub: Subject (user UUID)
- iss: Issuer (authentication service)
- aud: Audience (intended recipients)
- iat: Issued At (token creation time)
- exp: Expiration Time
- nbf: Not Before (optional, token not valid before this time)
Custom Claims:
- sid: Session ID (for tracking user sessions)
- usr: Username (human-readable identifier)
- rls: Roles (authorization roles for RBAC)
- typ: Token Type (always "access" for access tokens)
- mle: Maximum Lifetime Expiry (absolute expiration)
type AccessTokenResponse ยถ
type AccessTokenResponse struct {
// Subject is the user's unique identifier (UUID).
Subject uuid.UUID `json:"sub"`
// SessionID uniquely identifies the user's session (UUID).
SessionID uuid.UUID `json:"sid"`
// Token is the signed JWT string ready for use in Authorization headers.
Token string `json:"tok"`
// Issuer identifies the authentication service.
Issuer string `json:"iss"`
// Username is the human-readable username.
Username string `json:"usr"`
// Roles contains the authorization roles for this token.
Roles []string `json:"rls"`
// Audience lists the intended recipients of this token.
Audience []string `json:"aud"`
// IssuedAt is when the token was created (RFC3339 format).
IssuedAt time.Time `json:"iat"`
// ExpiresAt is when the token expires (RFC3339 format).
ExpiresAt time.Time `json:"exp"`
// NotBefore is when the token becomes valid (RFC3339 format).
NotBefore time.Time `json:"nbf"`
// MaxLifetimeExpiry is the absolute expiration time (RFC3339 format).
MaxLifetimeExpiry time.Time `json:"mle"`
// TokenType is always "access".
TokenType TokenType `json:"typ"`
}
AccessTokenResponse contains the generated access token and its associated metadata. This is returned after successful token creation and includes all information needed for the client to use the token.
type BenchRepositoryFactory ยถ added in v1.0.6
type BenchRepositoryFactory func(b *testing.B) (TokenRepository, func())
type GormTokenRepository ยถ added in v1.0.6
type GormTokenRepository struct {
// contains filtered or unexported fields
}
GormTokenRepository implements TokenRepository using GORM with SQL database. This repository provides persistent storage for token revocation and rotation data.
Supported Databases:
- PostgreSQL (recommended for production)
- MySQL/MariaDB
- SQLite (development only)
- SQL Server
- CockroachDB
Architecture Features:
- Connection pooling via GORM configuration
- Automatic migrations for schema management
- UPSERT operations for atomic updates
- Composite indexes for optimal query performance
- Connection health checking and validation
Performance Characteristics:
- Sub-millisecond reads with proper indexing
- Efficient batch operations for cleanup
- Connection pooling reduces overhead
- Transaction support for data consistency
Production Considerations:
- Configure connection pooling in GORM
- Monitor database performance metrics
- Set up database backup strategies
- Implement connection retry logic
- Consider read replicas for high read throughput
func (*GormTokenRepository) CleanupAll ยถ added in v1.0.6
func (r *GormTokenRepository) CleanupAll(ctx context.Context) error
CleanupAll removes all expired tokens (both revoked and rotated) in one operation. Convenience method for comprehensive maintenance.
Operation Sequence:
- Cleanup expired access tokens
- Cleanup expired refresh tokens
- Cleanup expired rotated tokens
Error Handling:
- Continues with next cleanup if one fails
- Returns first error encountered
- Provides detailed error context for each operation
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- error: If any cleanup operation fails, with detailed context
Example (Comprehensive cleanup):
// Run full cleanup daily
err := repo.CleanupAll(ctx)
if err != nil {
log.Printf("Comprehensive cleanup failed: %v", err)
} else {
log.Println("All expired tokens cleaned up successfully")
}
func (*GormTokenRepository) CleanupExpiredRevokedTokens ยถ added in v1.0.6
func (r *GormTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
CleanupExpiredRevokedTokens removes expired revoked tokens from the database. Batch DELETE operations are efficient and don't need explicit transactions.
Performance Characteristics:
- Single DELETE statement with WHERE clause
- Database handles deletion atomically
- Index on expires_at enables efficient range scans
- Batch operation minimizes database round trips
Maintenance Considerations:
- Run periodically to prevent table bloat
- Monitor deletion performance on large tables
- Consider time-based partitioning for very large datasets
- Schedule during low-traffic periods for production
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of tokens to cleanup (AccessToken or RefreshToken)
Returns:
- error: If token type is invalid or database operation fails
Database Operation:
DELETE FROM revoked_tokens WHERE token_type = ? AND expires_at <= ?
Example (Scheduled cleanup):
// Run cleanup every hour
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := repo.CleanupExpiredRevokedTokens(ctx, AccessToken)
cancel()
if err != nil {
log.Printf("Cleanup failed: %v", err)
}
}
func (*GormTokenRepository) CleanupExpiredRotatedTokens ยถ added in v1.0.6
func (r *GormTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
CleanupExpiredRotatedTokens removes expired rotated tokens from the database. Efficient batch operation using TTL index on expires_at.
Performance Optimizations:
- Single DELETE statement for all expired rotated tokens
- Index on expires_at enables efficient range query
- Database handles atomic deletion
- Minimal locking for read-heavy workloads
Maintenance Scheduling:
- Run less frequently than revoked token cleanup
- Rotated tokens typically have longer TTL
- Consider running daily or weekly
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- error: If database operation fails
Database Operation:
DELETE FROM rotated_tokens WHERE expires_at <= ?
Example (Weekly cleanup):
// Run cleanup once per week
go func() {
for range time.Tick(7*24*time.Hour) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
err := repo.CleanupExpiredRotatedTokens(ctx)
cancel()
if err != nil {
log.Printf("Rotated token cleanup failed: %v", err)
}
}
}()
func (*GormTokenRepository) Close ยถ added in v1.0.6
func (r *GormTokenRepository) Close() error
Close performs cleanup operations and closes the database connection. Implements graceful shutdown pattern for resource cleanup.
Cleanup Actions:
- Closes underlying database connection pool
- Releases all database connections
- Prevents connection leaks during application shutdown
Important: This should be called during application shutdown to prevent database connection leaks and ensure graceful termination.
Returns:
- error: If closing the database connection fails
Example (Graceful shutdown):
// Setup signal handling
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Wait for shutdown signal
<-ctx.Done()
// Close repository during shutdown
if err := repo.Close(); err != nil {
log.Printf("Failed to close token repository: %v", err)
}
func (*GormTokenRepository) GetRotationTTL ยถ added in v1.0.6
func (r *GormTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
GetRotationTTL returns the remaining TTL for a rotated token. Useful for debugging, monitoring, and cache optimization.
Use Cases:
- Debugging rotation-related issues
- Monitoring token rotation patterns
- Optimizing cleanup scheduling
- Understanding token lifecycle
Parameters:
- ctx: Context for cancellation and timeout
- token: The token to check for remaining TTL
Returns:
- time.Duration: Remaining TTL if token is rotated and not expired, 0 otherwise
- error: If token is empty or database operation fails
Database Query:
SELECT expires_at FROM rotated_tokens WHERE token_hash = ?
Example (Monitor rotation TTL):
ttl, err := repo.GetRotationTTL(ctx, "rotated-token")
if err != nil {
return fmt.Errorf("failed to get rotation TTL: %w", err)
}
if ttl > 0 {
log.Printf("Token will be automatically cleaned up in %v", ttl)
}
func (*GormTokenRepository) IsTokenRevoked ยถ added in v1.0.6
func (r *GormTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
IsTokenRevoked checks if a token has been revoked by checking its hash. Performs efficient database lookup using composite index.
Performance Optimizations:
- Uses composite index (token_hash, token_type) for fast lookups
- Additional filter on expires_at for automatic cleanup
- COUNT operation is optimized at database level
- Read-only operation with minimal locking
Security Considerations:
- Only compares token hashes, never the actual tokens
- Automatic expiration check prevents false positives
- Database-level validation ensures data integrity
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of token to check (AccessToken or RefreshToken)
- token: The token string to check for revocation
Returns:
- bool: True if token is revoked and not expired, false otherwise
- error: If token is empty, token type is invalid, or database operation fails
Database Query:
SELECT COUNT(*) FROM revoked_tokens WHERE token_hash = ? AND token_type = ? AND expires_at > ?
Example (Check access token):
revoked, err := repo.IsTokenRevoked(ctx, AccessToken, "eyJhbGciOiJIUzI1NiIs...")
if err != nil {
return fmt.Errorf("failed to check token revocation: %w", err)
}
if revoked {
return errors.New("token has been revoked")
}
Example (Check refresh token):
revoked, err := repo.IsTokenRevoked(ctx, RefreshToken, "refresh-token-here")
if err != nil {
return fmt.Errorf("failed to check refresh token: %w", err)
}
func (*GormTokenRepository) IsTokenRotated ยถ added in v1.0.6
IsTokenRotated checks if a token has been rotated by checking its hash. Performs efficient database lookup with automatic expiration filtering.
Security Purpose:
- Detects if a refresh token has been previously rotated
- Prevents reuse of rotated tokens in replay attacks
- Essential for rotation-based security schemes
Performance Optimizations:
- Uses unique index on token_hash for fast lookups
- Automatic expiration filtering at database level
- COUNT operation is database-optimized
Parameters:
- ctx: Context for cancellation and timeout
- token: The token to check for rotation status
Returns:
- bool: True if token has been rotated and not expired, false otherwise
- error: If token is empty or database operation fails
Database Query:
SELECT COUNT(*) FROM rotated_tokens WHERE token_hash = ? AND expires_at > ?
Example (Check rotation status):
rotated, err := repo.IsTokenRotated(ctx, "suspect-refresh-token")
if err != nil {
return fmt.Errorf("failed to check rotation: %w", err)
}
if rotated {
return errors.New("token has been rotated - do not accept")
}
func (*GormTokenRepository) MarkTokenRevoke ยถ added in v1.0.6
func (r *GormTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
MarkTokenRevoke marks a token as revoked by storing its hash. Uses atomic UPSERT operation to handle concurrent revocation attempts.
Security Implementation:
- Token is hashed using SHA-256 before storage
- Composite unique key prevents duplicate entries
- TTL-based automatic expiration
- No actual tokens stored in database
Performance Characteristics:
- Single database write operation
- Atomic UPSERT prevents race conditions
- Composite index enables fast conflict detection
- No explicit transaction needed (UPSERT is atomic)
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of token (AccessToken or RefreshToken)
- token: The actual token string to revoke
- ttl: Time-to-live duration for the revocation record
Returns:
- error: If token is empty, TTL is invalid, token type is invalid, or database operation fails
Database Operation:
INSERT INTO revoked_tokens (token_hash, token_type, expires_at, created_at) VALUES (?, ?, ?, ?) ON CONFLICT (token_hash, token_type) DO UPDATE SET expires_at = EXCLUDED.expires_at
Example (Revoke access token):
err := repo.MarkTokenRevoke(ctx, AccessToken, "eyJhbGciOiJIUzI1NiIs...", 24*time.Hour)
if err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
Example (Revoke refresh token with shorter TTL):
err := repo.MarkTokenRevoke(ctx, RefreshToken, "refresh-token-here", 7*24*time.Hour)
if err != nil {
return fmt.Errorf("failed to revoke refresh token: %w", err)
}
func (*GormTokenRepository) MarkTokenRotated ยถ added in v1.0.6
func (r *GormTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
MarkTokenRotated marks a token as rotated by storing its hash. Uses atomic UPSERT operation for idempotent rotation tracking.
Security Purpose:
- Prevents replay of rotated refresh tokens
- Enables one-time use during token rotation flow
- Protects against token replay attacks
Performance Characteristics:
- Single database write operation
- Atomic UPSERT handles concurrent rotation attempts
- Unique index enables fast conflict detection
Parameters:
- ctx: Context for cancellation and timeout
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record (typically matches refresh token expiry)
Returns:
- error: If token is empty, TTL is invalid, or database operation fails
Database Operation:
INSERT INTO rotated_tokens (token_hash, expires_at, created_at) VALUES (?, ?, ?) ON CONFLICT (token_hash) DO UPDATE SET expires_at = EXCLUDED.expires_at
Example (Mark token as rotated):
err := repo.MarkTokenRotated(ctx, "old-refresh-token", 7*24*time.Hour)
if err != nil {
return fmt.Errorf("failed to mark token as rotated: %w", err)
}
func (*GormTokenRepository) MarkTokenRotatedAtomic ยถ added in v1.0.6
func (r *GormTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
MarkTokenRotatedAtomic marks a token as rotated atomically, returning whether it was newly rotated. This method provides true atomicity for rotation detection in concurrent scenarios.
Use Cases:
- Preventing race conditions during concurrent token rotation
- Ensuring exactly-once rotation semantics
- Distributed system environments with multiple token validators
Key Difference from MarkTokenRotated:
- Returns boolean indicating if rotation was actually performed
- Uses DO NOTHING on conflict instead of updating
- Essential for preventing double-spending in rotation flows
Parameters:
- ctx: Context for cancellation and timeout
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- bool: True if token was newly rotated, false if already rotated
- error: If token is empty, TTL is invalid, or database operation fails
Database Operation:
INSERT INTO rotated_tokens (token_hash, expires_at, created_at) VALUES (?, ?, ?) ON CONFLICT (token_hash) DO NOTHING
Example (Atomic rotation check):
rotated, err := repo.MarkTokenRotatedAtomic(ctx, "refresh-token", 24*time.Hour)
if err != nil {
return fmt.Errorf("rotation failed: %w", err)
}
if !rotated {
return errors.New("token was already rotated - potential replay attack")
}
// Proceed with issuing new tokens
func (*GormTokenRepository) Stats ยถ added in v1.0.6
func (r *GormTokenRepository) Stats(ctx context.Context) (map[string]interface{}, error)
Stats returns statistics about the repository for monitoring and debugging. Provides insights into token revocation and rotation patterns.
Metrics Collected:
- Total revoked tokens (all types)
- Revoked access tokens count
- Revoked refresh tokens count
- Rotated tokens count
Use Cases:
- Monitoring system health and usage patterns
- Capacity planning and performance tuning
- Security auditing and anomaly detection
- Debugging token-related issues
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- map[string]interface{}: Dictionary of statistics metrics
- error: If any database count operation fails
Example (Prometheus metrics):
stats, err := repo.Stats(ctx)
if err != nil {
log.Printf("Failed to get stats: %v", err)
return
}
// Export to monitoring system
revokedTotal.Set(float64(stats["total_revoked_tokens"].(int64)))
rotatedCount.Set(float64(stats["rotated_tokens"].(int64)))
type GourdianTokenConfig ยถ
type GourdianTokenConfig struct {
// RotationEnabled determines whether refresh token rotation is enforced.
// When enabled, each refresh token can only be used once to obtain a new token.
// Prevents token reuse attacks and improves security.
RotationEnabled bool
// RevocationEnabled determines whether tokens can be explicitly revoked before expiration.
// When enabled, requires a TokenRepository to track revoked tokens.
// Essential for logout functionality and compromised token mitigation.
RevocationEnabled bool
// Algorithm specifies the JWT signing algorithm.
// Supported: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA.
// Must match the SigningMethod (e.g., "HS256" requires Symmetric method).
Algorithm string
// SymmetricKey is the Base64-encoded secret key for HMAC algorithms.
// Required when SigningMethod is Symmetric. Must be at least 32 bytes.
// Keep this value secret and rotate it periodically.
SymmetricKey string
// PrivateKeyPath is the file path to the PEM-encoded private key.
// Required when SigningMethod is Asymmetric.
// File should have restrictive permissions (0600 recommended).
PrivateKeyPath string
// PublicKeyPath is the file path to the PEM-encoded public key or certificate.
// Required when SigningMethod is Asymmetric.
// Used for token verification and can be distributed to services that need to validate tokens.
PublicKeyPath string
// Issuer identifies the token issuer (e.g., "auth.example.com").
// Included in the "iss" claim and validated during token verification.
Issuer string
// Audience specifies the intended recipients of the token (e.g., ["api.example.com"]).
// Included in the "aud" claim. Validators should check this matches their service identifier.
Audience []string
// AllowedAlgorithms is a whitelist of acceptable algorithms for token verification.
// Helps prevent algorithm confusion attacks. If empty, all supported algorithms are allowed.
AllowedAlgorithms []string
// RequiredClaims lists mandatory claims that must be present in tokens.
// Standard required claims include: "iss", "aud", "nbf", "mle".
RequiredClaims []string
// SigningMethod specifies whether to use Symmetric (HMAC) or Asymmetric (RSA/ECDSA) signing.
SigningMethod SigningMethod
// AccessExpiryDuration is the time until an access token expires after issuance.
// Typical values: 15-30 minutes. Shorter durations improve security but increase refresh frequency.
AccessExpiryDuration time.Duration
// AccessMaxLifetimeExpiry is the absolute maximum validity period from token creation.
// Even if refreshed, tokens cannot be valid beyond this time.
// Must be greater than or equal to AccessExpiryDuration.
AccessMaxLifetimeExpiry time.Duration
// RefreshExpiryDuration is the time until a refresh token expires after issuance.
// Typical values: 7-30 days. Balance between security and user convenience.
RefreshExpiryDuration time.Duration
// RefreshMaxLifetimeExpiry is the absolute maximum validity period from token creation.
// Enforces periodic re-authentication. Must be greater than or equal to RefreshExpiryDuration.
RefreshMaxLifetimeExpiry time.Duration
// RefreshReuseInterval is the minimum time between refresh token reuse attempts.
// Helps detect suspicious activity. Set to 0 to disable. Typical value: 5 minutes.
RefreshReuseInterval time.Duration
// CleanupInterval determines how often expired tokens are removed from storage.
// Prevents database bloat. Typical values: 1-6 hours. Must be at least 1 minute.
CleanupInterval time.Duration
}
GourdianTokenConfig holds the configuration for token generation, validation, and lifecycle management. All duration fields must be positive values. Zero or negative durations will cause validation errors.
Security Considerations:
- SymmetricKey must be at least 32 bytes for HMAC algorithms
- Private key files should have 0600 permissions
- Algorithm must match the SigningMethod (e.g., HS256 for Symmetric, RS256 for Asymmetric)
- Consider enabling both RotationEnabled and RevocationEnabled for production systems
func DefaultGourdianTokenConfig ยถ added in v0.0.3
func DefaultGourdianTokenConfig(symmetricKey string) GourdianTokenConfig
DefaultGourdianTokenConfig creates a token configuration with sensible defaults for quick setup. Uses symmetric HMAC-SHA256 signing with moderate security settings suitable for development and testing.
Default Configuration:
- Algorithm: HS256 (HMAC-SHA256)
- SigningMethod: Symmetric
- RevocationEnabled: false
- RotationEnabled: false
- Issuer: "gourdian.com"
- AllowedAlgorithms: ["HS256", "HS384", "HS512", "RS256", "ES256", "PS256"]
- RequiredClaims: ["iss", "aud", "nbf", "mle"]
- AccessExpiryDuration: 30 minutes
- AccessMaxLifetimeExpiry: 24 hours
- RefreshExpiryDuration: 7 days
- RefreshMaxLifetimeExpiry: 30 days
- RefreshReuseInterval: 5 minutes
- CleanupInterval: 6 hours
Parameters:
- symmetricKey: The secret key for HMAC signing (must be at least 32 bytes)
Returns:
- GourdianTokenConfig: Pre-configured token configuration with defaults
Security Note:
For production systems, consider: - Using a stronger algorithm (HS384 or HS512) - Enabling RevocationEnabled and RotationEnabled - Reducing AccessExpiryDuration to 15 minutes - Using asymmetric signing for distributed systems
Example:
config := DefaultGourdianTokenConfig("your-secret-key-at-least-32-bytes")
func DefaultTestConfig ยถ added in v1.0.6
func DefaultTestConfig() GourdianTokenConfig
func NewGourdianTokenConfig ยถ
func NewGourdianTokenConfig( signingMethod SigningMethod, rotationEnabled, revocationEnabled bool, audience, allowedAlgorithms, requiredClaims []string, algorithm, symmetricKey, privateKeyPath, publicKeyPath, issuer string, accessExpiryDuration, accessMaxLifetimeExpiry, refreshExpiryDuration, refreshMaxLifetimeExpiry, refreshReuseInterval, cleanupInterval time.Duration, ) GourdianTokenConfig
NewGourdianTokenConfig creates a new token configuration with all parameters explicitly specified. This constructor provides full control over all configuration options.
Parameters:
- signingMethod: Cryptographic method (Symmetric or Asymmetric)
- rotationEnabled: Enable refresh token rotation to prevent reuse
- revocationEnabled: Enable explicit token revocation before expiration
- audience: List of intended token recipients (e.g., ["api.example.com"])
- allowedAlgorithms: Whitelist of acceptable signing algorithms
- requiredClaims: List of mandatory claims that must be present
- algorithm: JWT signing algorithm (must match signingMethod)
- symmetricKey: Secret key for HMAC (required if signingMethod is Symmetric)
- privateKeyPath: Path to private key file (required if signingMethod is Asymmetric)
- publicKeyPath: Path to public key file (required if signingMethod is Asymmetric)
- issuer: Token issuer identifier
- accessExpiryDuration: Access token lifetime (e.g., 30 minutes)
- accessMaxLifetimeExpiry: Maximum access token validity (e.g., 24 hours)
- refreshExpiryDuration: Refresh token lifetime (e.g., 7 days)
- refreshMaxLifetimeExpiry: Maximum refresh token validity (e.g., 30 days)
- refreshReuseInterval: Minimum time between reuse attempts (e.g., 5 minutes)
- cleanupInterval: Frequency of expired token cleanup (e.g., 6 hours)
Returns:
- GourdianTokenConfig: Fully configured token configuration
Example:
config := NewGourdianTokenConfig(
gourdiantoken.Symmetric,
true, true,
[]string{"api.example.com"},
[]string{"HS256", "HS384"},
[]string{"iss", "aud", "nbf", "mle"},
"HS256",
"your-secret-key-min-32-bytes-long",
"", "",
"auth.example.com",
30*time.Minute, 24*time.Hour,
7*24*time.Hour, 30*24*time.Hour,
5*time.Minute, 6*time.Hour,
)
type GourdianTokenMaker ยถ
type GourdianTokenMaker interface {
// CreateAccessToken generates a new signed access token with the specified claims.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - userID: The user's unique identifier (must not be uuid.Nil)
// - username: Human-readable username (max 1024 characters)
// - roles: Authorization roles (must contain at least one non-empty role)
// - sessionID: Session identifier for tracking
//
// Returns:
// - *AccessTokenResponse: Generated token with metadata
// - error: If token creation fails, parameters are invalid, or context is cancelled
//
// Example:
//
// token, err := maker.CreateAccessToken(
// ctx,
// uuid.MustParse("123e4567-e89b-12d3-a456-426614174000"),
// "john.doe",
// []string{"user", "admin"},
// sessionUUID,
// )
CreateAccessToken(ctx context.Context, userID uuid.UUID, username string, roles []string, sessionID uuid.UUID) (*AccessTokenResponse, error)
// CreateRefreshToken generates a new signed refresh token.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - userID: The user's unique identifier (must not be uuid.Nil)
// - username: Human-readable username (max 1024 characters)
// - sessionID: Session identifier for tracking
//
// Returns:
// - *RefreshTokenResponse: Generated token with metadata
// - error: If token creation fails, parameters are invalid, or context is cancelled
//
// Example:
//
// token, err := maker.CreateRefreshToken(
// ctx,
// userUUID,
// "john.doe",
// sessionUUID,
// )
CreateRefreshToken(ctx context.Context, userID uuid.UUID, username string, sessionID uuid.UUID) (*RefreshTokenResponse, error)
// VerifyAccessToken validates an access token and returns its claims.
// Checks signature, expiration, revocation status, and required claims.
//
// Validation includes:
// - Cryptographic signature verification
// - Expiration time check
// - Not-before time check
// - Maximum lifetime check
// - Revocation status (if enabled)
// - Required claims presence
// - Token type validation
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - tokenString: The JWT token string to verify
//
// Returns:
// - *AccessTokenClaims: Parsed and validated token claims
// - error: If token is invalid, expired, revoked, or context is cancelled
//
// Example:
//
// claims, err := maker.VerifyAccessToken(ctx, tokenString)
// if err != nil {
// // Token is invalid, expired, or revoked
// return err
// }
// // Use claims.Roles for authorization
VerifyAccessToken(ctx context.Context, tokenString string) (*AccessTokenClaims, error)
// VerifyRefreshToken validates a refresh token and returns its claims.
// Checks signature, expiration, revocation status, rotation status, and required claims.
//
// Validation includes:
// - Cryptographic signature verification
// - Expiration time check
// - Not-before time check
// - Maximum lifetime check
// - Revocation status (if enabled)
// - Rotation status (if enabled)
// - Required claims presence
// - Token type validation
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - tokenString: The JWT token string to verify
//
// Returns:
// - *RefreshTokenClaims: Parsed and validated token claims
// - error: If token is invalid, expired, revoked, rotated, or context is cancelled
//
// Example:
//
// claims, err := maker.VerifyRefreshToken(ctx, tokenString)
// if err != nil {
// // Token is invalid, require re-authentication
// return err
// }
VerifyRefreshToken(ctx context.Context, tokenString string) (*RefreshTokenClaims, error)
// RevokeAccessToken marks an access token as revoked, preventing further use.
// Requires RevocationEnabled to be true in the configuration.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - token: The JWT token string to revoke
//
// Returns:
// - error: If revocation is disabled, token is invalid, or operation fails
//
// Use Cases:
// - User logout
// - Token compromise
// - Administrative revocation
//
// Example:
//
// err := maker.RevokeAccessToken(ctx, tokenString)
RevokeAccessToken(ctx context.Context, token string) error
// RevokeRefreshToken marks a refresh token as revoked, preventing further use.
// Requires RevocationEnabled to be true in the configuration.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - token: The JWT token string to revoke
//
// Returns:
// - error: If revocation is disabled, token is invalid, or operation fails
//
// Example:
//
// err := maker.RevokeRefreshToken(ctx, tokenString)
RevokeRefreshToken(ctx context.Context, token string) error
// RotateRefreshToken exchanges an old refresh token for a new one.
// The old token is atomically marked as rotated and cannot be reused.
// Requires RotationEnabled to be true in the configuration.
//
// Rotation Process:
// 1. Verifies the old token is valid
// 2. Atomically marks the old token as rotated
// 3. Creates a new refresh token with the same user/session
//
// Security Benefits:
// - Prevents token reuse attacks
// - Detects token theft (multiple rotation attempts)
// - Limits token lifetime even if compromised
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - oldToken: The current refresh token to rotate
//
// Returns:
// - *RefreshTokenResponse: New refresh token with updated expiration
// - error: If rotation is disabled, old token is invalid/rotated, or operation fails
//
// Example:
//
// newToken, err := maker.RotateRefreshToken(ctx, oldTokenString)
// if err != nil {
// // Token already rotated or invalid, possible attack
// return err
// }
// // Return newToken to client
RotateRefreshToken(ctx context.Context, oldToken string) (*RefreshTokenResponse, error)
}
GourdianTokenMaker is the main interface for token operations. Implementations handle token creation, verification, revocation, and rotation with support for multiple signing algorithms and security features.
Thread Safety:
All methods are safe for concurrent use by multiple goroutines.
Context Handling:
All methods accept a context.Context for cancellation and timeout support. Operations will return an error if the context is cancelled.
func DefaultGourdianTokenMaker ยถ added in v1.0.3
func DefaultGourdianTokenMaker( ctx context.Context, symmetricKey string, tokenRepo TokenRepository, ) (GourdianTokenMaker, error)
DefaultGourdianTokenMaker creates a token maker with sensible defaults for quick setup. Uses symmetric HMAC-SHA256 signing with minimal configuration required.
Default Configuration:
- Algorithm: HS256 (HMAC-SHA256)
- Issuer: "gourdian.com"
- AllowedAlgorithms: ["HS256", "RS256", "ES256", "PS256"]
- RequiredClaims: ["iss", "aud", "nbf", "mle"]
- AccessExpiryDuration: 30 minutes
- AccessMaxLifetimeExpiry: 24 hours
- RefreshExpiryDuration: 7 days
- RefreshMaxLifetimeExpiry: 30 days
- RefreshReuseInterval: 5 minutes
- CleanupInterval: 6 hours
Automatic Feature Detection:
- If tokenRepo is provided: RevocationEnabled and RotationEnabled are set to true
- If tokenRepo is nil: RevocationEnabled and RotationEnabled are set to false
Parameters:
- ctx: Context for initialization (cancellation support)
- symmetricKey: Secret key for HMAC signing (must be at least 32 bytes)
- tokenRepo: Optional token repository. If provided, enables revocation and rotation. Pass nil for stateless operation without these features.
Returns:
- GourdianTokenMaker: Configured token maker with default settings
- error: If initialization fails or context is cancelled
Use Cases:
- Rapid prototyping and development
- Simple authentication systems
- Microservices with symmetric signing
- Getting started with JWT tokens
Security Considerations:
For production systems, consider using NewGourdianTokenMaker with: - Asymmetric signing for distributed systems - Shorter access token durations (15 minutes) - Custom audience and issuer values - Stricter allowed algorithms list
Example (Stateless - no repository):
maker, err := gourdiantoken.DefaultGourdianTokenMaker(
ctx,
"your-secret-key-at-least-32-bytes-long",
nil, // No token repository, stateless operation
)
if err != nil {
log.Fatal(err)
}
// Revocation and rotation are disabled
Example (With repository - rotation and revocation enabled):
redisRepo := NewRedisTokenRepository(redisClient)
maker, err := gourdiantoken.DefaultGourdianTokenMaker(
ctx,
"your-secret-key-at-least-32-bytes-long",
redisRepo, // Token repository provided
)
if err != nil {
log.Fatal(err)
}
// Revocation and rotation are automatically enabled
func NewGourdianTokenMaker ยถ
func NewGourdianTokenMaker(ctx context.Context, config GourdianTokenConfig, tokenRepo TokenRepository) (GourdianTokenMaker, error)
NewGourdianTokenMaker creates a new GourdianTokenMaker with the specified configuration and token repository. This is the main constructor that provides full control over token maker initialization.
Initialization Process:
- Validates the configuration for security and consistency
- Checks algorithm and signing method compatibility
- Initializes cryptographic keys (loads from files for asymmetric)
- Sets up background cleanup goroutines if rotation/revocation enabled
- Returns a fully configured token maker ready for use
Background Operations:
If rotation or revocation is enabled, background goroutines are started to: - Clean up expired revoked tokens (prevents memory leaks) - Clean up expired rotation markers (prevents memory leaks) These goroutines run at intervals specified by config.CleanupInterval
Parameters:
- ctx: Context for initialization. If cancelled, initialization fails immediately.
- config: Token maker configuration (see GourdianTokenConfig for details)
- tokenRepo: Repository for token storage. Required if RevocationEnabled or RotationEnabled is true. Can be nil for stateless operation (revocation and rotation must be disabled).
Returns:
- GourdianTokenMaker: Configured token maker instance ready for production use
- error: If configuration is invalid, keys cannot be loaded, context is cancelled, or repository is required but nil
Configuration Validation:
- Checks signing method matches algorithm (e.g., HS256 requires Symmetric)
- Validates key files exist and have secure permissions (0600 for private keys)
- Ensures durations are positive and logical (expiry < max lifetime)
- Verifies required parameters are provided for the chosen signing method
Example (Symmetric with rotation and revocation):
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "your-secret-key-at-least-32-bytes-long",
Issuer: "auth.example.com",
Audience: []string{"api.example.com"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 30 * time.Minute,
AccessMaxLifetimeExpiry: 24 * time.Hour,
RefreshExpiryDuration: 7 * 24 * time.Hour,
RefreshMaxLifetimeExpiry: 30 * 24 * time.Hour,
CleanupInterval: 6 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMaker(ctx, config, tokenRepo)
if err != nil {
log.Fatal(err)
}
Example (Asymmetric with RSA):
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Asymmetric,
Algorithm: "RS256",
PrivateKeyPath: "/path/to/private.pem",
PublicKeyPath: "/path/to/public.pem",
Issuer: "auth.example.com",
RevocationEnabled: false,
RotationEnabled: false,
AccessExpiryDuration: 15 * time.Minute,
RefreshExpiryDuration: 7 * 24 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMaker(ctx, config, nil)
func NewGourdianTokenMakerNoStorage ยถ added in v1.0.6
func NewGourdianTokenMakerNoStorage(ctx context.Context, config GourdianTokenConfig) (GourdianTokenMaker, error)
NewGourdianTokenMakerNoStorage creates a GourdianTokenMaker without any token storage backend. This is suitable for stateless token validation where token revocation and rotation are not needed. The token repository will be nil, so RevocationEnabled and RotationEnabled must be false.
Use Cases:
- Stateless microservices that only validate tokens
- Read-only API services
- Systems where all state is in the JWT itself
- High-performance scenarios where database lookups are not acceptable
- Distributed systems with no shared storage
Limitations:
- Cannot revoke tokens before expiration
- Cannot rotate refresh tokens
- Compromised tokens remain valid until natural expiration
- No logout functionality (unless expiry is very short)
Security Implications:
Without revocation/rotation: - Use shorter token lifetimes (e.g., 15 minutes for access tokens) - Implement complementary security measures (rate limiting, monitoring) - Consider using asymmetric signing for better key distribution - Monitor for suspicious patterns and failed validation attempts
Parameters:
- ctx: Context for initialization (cancellation support)
- config: Configuration for the token maker. Must have RevocationEnabled and RotationEnabled set to false, otherwise this function returns an error.
Returns:
- GourdianTokenMaker: A configured token maker instance without storage backend
- error: If configuration is invalid, revocation/rotation is enabled, or context is cancelled
Configuration Requirements:
- RevocationEnabled must be false
- RotationEnabled must be false
- All other configuration options are validated normally
Example (Symmetric signing):
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "your-secret-key-at-least-32-bytes-long",
Issuer: "auth.example.com",
Audience: []string{"api.example.com"},
RevocationEnabled: false, // Required for no-storage mode
RotationEnabled: false, // Required for no-storage mode
AccessExpiryDuration: 15 * time.Minute, // Shorter for security
RefreshExpiryDuration: 7 * 24 * time.Hour,
CleanupInterval: 6 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerNoStorage(ctx, config)
if err != nil {
log.Fatal(err)
}
Example (Asymmetric signing for microservices):
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Asymmetric,
Algorithm: "RS256",
PrivateKeyPath: "/keys/private.pem",
PublicKeyPath: "/keys/public.pem",
Issuer: "auth.example.com",
Audience: []string{"api.example.com", "service.example.com"},
RevocationEnabled: false,
RotationEnabled: false,
AccessExpiryDuration: 15 * time.Minute,
AccessMaxLifetimeExpiry: 24 * time.Hour,
RefreshExpiryDuration: 24 * time.Hour,
RefreshMaxLifetimeExpiry: 30 * 24 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerNoStorage(ctx, config)
func NewGourdianTokenMakerWithGorm ยถ added in v1.0.6
func NewGourdianTokenMakerWithGorm(ctx context.Context, config GourdianTokenConfig, db *gorm.DB) (GourdianTokenMaker, error)
NewGourdianTokenMakerWithGorm creates a GourdianTokenMaker with a GORM-based token repository. This supports any database that GORM supports (PostgreSQL, MySQL, SQLite, etc.). Suitable for production deployments with persistent token revocation and rotation tracking.
Use Cases:
- Production applications with existing SQL databases
- Applications requiring ACID compliance for token operations
- Systems with complex relational data models
- Environments where SQL expertise exists
- Applications requiring complex queries for token analytics
Supported Databases:
- PostgreSQL (recommended for production)
- MySQL/MariaDB
- SQLite (development only)
- SQL Server
- CockroachDB
Performance Characteristics:
- Good read/write performance with proper indexing
- Network latency to database
- Supports connection pooling
- Transaction support for data consistency
Setup Requirements:
- Database migrations run automatically
- Proper indexes created for performance
- Database connection pooling configured
- Regular maintenance (vacuum/optimize) for SQL databases
Parameters:
- ctx: Context for initialization (cancellation, timeout support)
- config: Configuration for the token maker
- db: Initialized GORM database instance with connection to target database
Returns:
- GourdianTokenMaker: A configured token maker instance with GORM storage
- error: If database connection fails, migration fails, configuration is invalid, or context is cancelled
Example (PostgreSQL production):
// Initialize GORM first
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Asymmetric,
Algorithm: "RS256",
PrivateKeyPath: "/app/keys/private.pem",
PublicKeyPath: "/app/keys/public.pem",
Issuer: "auth.production.com",
Audience: []string{"api.production.com", "admin.production.com"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 15 * time.Minute,
AccessMaxLifetimeExpiry: 24 * time.Hour,
RefreshExpiryDuration: 7 * 24 * time.Hour,
RefreshMaxLifetimeExpiry: 30 * 24 * time.Hour,
CleanupInterval: 24 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithGorm(ctx, config, gormDB)
if err != nil {
log.Fatal(err)
}
Example (SQLite development):
gormDB, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "dev-key-for-sqlite-testing",
Issuer: "dev-auth",
Audience: []string{"dev-api"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 1 * time.Hour,
RefreshExpiryDuration: 24 * time.Hour,
CleanupInterval: 6 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithGorm(ctx, config, gormDB)
func NewGourdianTokenMakerWithMemory ยถ added in v1.0.6
func NewGourdianTokenMakerWithMemory(ctx context.Context, config GourdianTokenConfig) (GourdianTokenMaker, error)
NewGourdianTokenMakerWithMemory creates a GourdianTokenMaker with an in-memory token repository. This is suitable for development, testing, or single-instance deployments. Token revocation and rotation data are stored in memory and will be lost on restart.
Use Cases:
- Development and testing environments
- Single-instance applications
- Prototyping and proof-of-concept implementations
- Applications where token persistence across restarts is not required
- Low-security internal applications
Performance Characteristics:
- Fastest storage backend (in-memory operations)
- No network latency
- Memory usage grows with active tokens
- Automatic cleanup via config.CleanupInterval
Limitations:
- Data lost on application restart
- Not suitable for distributed systems
- Memory consumption proportional to active tokens
- No persistence for audit or compliance requirements
Parameters:
- ctx: Context for initialization (cancellation, timeout support)
- config: Configuration for the token maker. CleanupInterval determines how often expired tokens are purged from memory.
Returns:
- GourdianTokenMaker: A configured token maker instance with in-memory storage
- error: If configuration is invalid, context is cancelled, or initialization fails
Example (Development setup):
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "dev-secret-key-for-testing-only",
Issuer: "dev-auth.local",
Audience: []string{"api.dev.local"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 1 * time.Hour,
RefreshExpiryDuration: 24 * time.Hour,
CleanupInterval: 1 * time.Hour, // Clean up every hour
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config)
if err != nil {
log.Fatal(err)
}
Example (Testing with short cleanup):
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "test-key",
Issuer: "test",
Audience: []string{"test-api"},
RevocationEnabled: true,
RotationEnabled: false,
AccessExpiryDuration: 15 * time.Minute,
RefreshExpiryDuration: 1 * time.Hour,
CleanupInterval: 5 * time.Minute, // Frequent cleanup for tests
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithMemory(ctx, config)
func NewGourdianTokenMakerWithMongo ยถ added in v1.0.6
func NewGourdianTokenMakerWithMongo(ctx context.Context, config GourdianTokenConfig, mongoDB *mongo.Database, transactionsEnabled bool) (GourdianTokenMaker, error)
NewGourdianTokenMakerWithMongo creates a GourdianTokenMaker with a MongoDB-based token repository. Suitable for production deployments using MongoDB for persistent token tracking. Transactions are enabled by default for consistency.
Use Cases:
- Production applications using MongoDB as primary database
- Document-oriented architectures
- High-write throughput scenarios
- Systems requiring horizontal scaling
- Applications with flexible schema requirements
Performance Characteristics:
- Excellent write performance
- Automatic sharding support
- TTL index for automatic token expiration
- Document-level atomic operations
- Built-in replication for high availability
Setup Requirements:
- MongoDB 4.0+ for transaction support (recommended 4.2+)
- TTL indexes created automatically
- Proper replica set configuration for production
- Connection string with appropriate read/write concerns
Parameters:
- ctx: Context for initialization (cancellation, timeout support)
- config: Configuration for the token maker
- mongoDB: Initialized MongoDB database instance from the official Mongo driver
Returns:
- GourdianTokenMaker: A configured token maker instance with MongoDB storage
- error: If MongoDB connection fails, index creation fails, configuration is invalid, or context is cancelled
Example (Production with transactions):
// Initialize MongoDB client first
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
if err != nil {
log.Fatal(err)
}
mongoDB := client.Database("auth_service")
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Asymmetric,
Algorithm: "RS256",
PrivateKeyPath: "/app/keys/private.pem",
PublicKeyPath: "/app/keys/public.pem",
Issuer: "auth.mongodb.example.com",
Audience: []string{"api.mongodb.example.com"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 15 * time.Minute,
AccessMaxLifetimeExpiry: 24 * time.Hour,
RefreshExpiryDuration: 7 * 24 * time.Hour,
RefreshMaxLifetimeExpiry: 30 * 24 * time.Hour,
CleanupInterval: 24 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithMongo(ctx, config, mongoDB)
if err != nil {
log.Fatal(err)
}
Example (Development without replica set):
// For development without replica set, transactions may be limited
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
mongoDB := client.Database("dev_auth")
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "mongo-dev-key-32-bytes-long-here",
Issuer: "dev-mongo-auth",
Audience: []string{"dev-api"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 1 * time.Hour,
RefreshExpiryDuration: 24 * time.Hour,
CleanupInterval: 12 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithMongo(ctx, config, mongoDB)
func NewGourdianTokenMakerWithRedis ยถ added in v1.0.6
func NewGourdianTokenMakerWithRedis(ctx context.Context, config GourdianTokenConfig, redisClient *redis.Client) (GourdianTokenMaker, error)
NewGourdianTokenMakerWithRedis creates a GourdianTokenMaker with a Redis-based token repository. Suitable for high-performance production deployments with Redis as the token store. Redis automatically handles TTL-based key expiration.
Use Cases:
- High-performance authentication systems
- Microservices architectures
- Distributed systems with shared token state
- Applications requiring sub-millisecond token validation
- Systems with high token revocation rates
Performance Characteristics:
- Sub-millisecond read/write operations
- Built-in TTL expiration
- In-memory performance with optional persistence
- Horizontal scaling via Redis Cluster
- Lua scripting for complex atomic operations
Redis Features Utilized:
- TTL (Time To Live) for automatic token expiration
- SETNX for atomic token creation
- Pipeline for batch operations
- Lua scripts for complex atomic operations
- Optional persistence (AOF/RDB) for durability
Setup Requirements:
- Redis 6.0+ recommended (for ACLs and improved Lua scripting)
- Proper memory configuration (maxmemory policy)
- Persistence configuration if token durability required
- Redis Sentinel or Cluster for high availability
Parameters:
- ctx: Context for initialization (cancellation, timeout support)
- config: Configuration for the token maker
- redisClient: Initialized Redis client from go-redis library
Returns:
- GourdianTokenMaker: A configured token maker instance with Redis storage
- error: If Redis connection fails, configuration is invalid, or context is cancelled
Example (Production with Redis Cluster):
// Initialize Redis client
redisClient := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"redis-node1:6379", "redis-node2:6379", "redis-node3:6379"},
Password: "your-redis-password",
PoolSize: 100,
})
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Asymmetric,
Algorithm: "RS256",
PrivateKeyPath: "/app/keys/private.pem",
PublicKeyPath: "/app/keys/public.pem",
Issuer: "auth.redis.example.com",
Audience: []string{"api.redis.example.com", "gateway.redis.example.com"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 15 * time.Minute,
AccessMaxLifetimeExpiry: 24 * time.Hour,
RefreshExpiryDuration: 7 * 24 * time.Hour,
RefreshMaxLifetimeExpiry: 30 * 24 * time.Hour,
CleanupInterval: 24 * time.Hour, // Less critical with Redis TTL
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient)
if err != nil {
log.Fatal(err)
}
Example (Single Redis instance for development):
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
config := gourdiantoken.GourdianTokenConfig{
SigningMethod: gourdiantoken.Symmetric,
Algorithm: "HS256",
SymmetricKey: "redis-dev-key-32-bytes-minimum",
Issuer: "dev-redis-auth",
Audience: []string{"dev-api"},
RevocationEnabled: true,
RotationEnabled: true,
AccessExpiryDuration: 30 * time.Minute,
RefreshExpiryDuration: 24 * time.Hour,
CleanupInterval: 6 * time.Hour,
}
maker, err := gourdiantoken.NewGourdianTokenMakerWithRedis(ctx, config, redisClient)
type JWTMaker ยถ
type JWTMaker struct {
// contains filtered or unexported fields
}
JWTMaker is the concrete implementation of GourdianTokenMaker. Handles JWT token lifecycle with configurable security features and multiple signing algorithms.
Thread Safety:
Safe for concurrent use. Internal state is immutable after initialization.
Lifecycle:
- Create with NewGourdianTokenMaker or DefaultGourdianTokenMaker
- Automatically starts background cleanup goroutines if rotation/revocation enabled
- Cleanup goroutines stop when the maker is garbage collected
func (*JWTMaker) CreateAccessToken ยถ
func (maker *JWTMaker) CreateAccessToken(ctx context.Context, userID uuid.UUID, username string, roles []string, sessionID uuid.UUID) (*AccessTokenResponse, error)
CreateAccessToken generates a new signed access token with the specified claims. Access tokens are short-lived and include user identity, session, and authorization roles.
Token Structure:
- Header: Algorithm and token type
- Payload: User claims (ID, username, roles, timestamps)
- Signature: Cryptographic signature for verification
Automatic Claims:
- jti: Unique token identifier (auto-generated UUIDv4)
- iat: Issued at timestamp (current time)
- exp: Expiration time (iat + AccessExpiryDuration)
- nbf: Not before time (current time)
- mle: Maximum lifetime expiry (iat + AccessMaxLifetimeExpiry)
- iss: Issuer from configuration
- aud: Audience from configuration
- typ: Token type ("access")
Validation:
- userID must not be uuid.Nil
- username must not exceed 1024 characters
- roles must contain at least one non-empty string
- sessionID can be any UUID (including Nil for sessionless tokens)
Parameters:
- ctx: Context for cancellation. Checks are performed before signing and during I/O.
- userID: The user's unique identifier (UUID, must not be Nil)
- username: Human-readable username (max 1024 characters, can be empty)
- roles: List of authorization roles (must contain at least one non-empty role)
- sessionID: Session identifier (UUID, can be Nil for sessionless operation)
Returns:
- *AccessTokenResponse: Complete token response with signed JWT and metadata
- error: If parameters are invalid, signing fails, or context is cancelled
Example (Basic usage):
userUUID := uuid.MustParse("123e4567-e89b-12d3-a456-426614174000")
sessionUUID := uuid.New()
token, err := maker.CreateAccessToken(
ctx,
userUUID,
"john.doe",
[]string{"user", "admin"},
sessionUUID,
)
if err != nil {
return fmt.Errorf("failed to create token: %w", err)
}
// Use token.Token in Authorization header
fmt.Printf("Token: %s\n", token.Token)
fmt.Printf("Expires: %s\n", token.ExpiresAt)
Example (Multiple roles):
token, err := maker.CreateAccessToken(
ctx,
userUUID,
"[email protected]",
[]string{"user", "admin", "moderator"},
sessionUUID,
)
Example (With context timeout):
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() token, err := maker.CreateAccessToken(ctx, userUUID, username, roles, sessionUUID)
func (*JWTMaker) CreateRefreshToken ยถ
func (maker *JWTMaker) CreateRefreshToken(ctx context.Context, userID uuid.UUID, username string, sessionID uuid.UUID) (*RefreshTokenResponse, error)
CreateRefreshToken generates a new signed refresh token for obtaining new access tokens. Refresh tokens are long-lived and do not include authorization roles.
Token Structure:
- Header: Algorithm and token type
- Payload: User identity and session (no roles)
- Signature: Cryptographic signature for verification
Automatic Claims:
- jti: Unique token identifier (auto-generated UUIDv4)
- iat: Issued at timestamp (current time)
- exp: Expiration time (iat + RefreshExpiryDuration)
- nbf: Not before time (current time)
- mle: Maximum lifetime expiry (iat + RefreshMaxLifetimeExpiry)
- iss: Issuer from configuration
- aud: Audience from configuration
- typ: Token type ("refresh")
Validation:
- userID must not be uuid.Nil
- username must not exceed 1024 characters
- sessionID can be any UUID (including Nil)
Parameters:
- ctx: Context for cancellation
- userID: The user's unique identifier (UUID, must not be Nil)
- username: Human-readable username (max 1024 characters)
- sessionID: Session identifier (UUID)
Returns:
- *RefreshTokenResponse: Complete token response with signed JWT and metadata
- error: If parameters are invalid, signing fails, or context is cancelled
Security Best Practices:
- Store refresh tokens securely (httpOnly cookies, secure storage)
- Use token rotation to prevent reuse
- Implement refresh token revocation for logout
- Monitor for suspicious refresh patterns
Example (Create refresh token):
refreshToken, err := maker.CreateRefreshToken(
ctx,
userUUID,
"john.doe",
sessionUUID,
)
if err != nil {
return fmt.Errorf("failed to create refresh token: %w", err)
}
// Store securely (e.g., httpOnly cookie)
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: refreshToken.Token,
Expires: refreshToken.ExpiresAt,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
Example (Create token pair):
accessToken, err := maker.CreateAccessToken(ctx, userUUID, username, roles, sessionUUID)
if err != nil {
return err
}
refreshToken, err := maker.CreateRefreshToken(ctx, userUUID, username, sessionUUID)
if err != nil {
return err
}
return &TokenPair{
AccessToken: accessToken.Token,
RefreshToken: refreshToken.Token,
}
func (*JWTMaker) RevokeAccessToken ยถ added in v1.0.2
RevokeAccessToken marks an access token as revoked, preventing its further use. The token will fail verification even if it hasn't expired yet. Requires RevocationEnabled to be true in the configuration.
Revocation Process:
- Parses the token to extract expiration time
- Calculates time-to-live (TTL) until natural expiration
- Stores token hash in repository with TTL
- Token verification will now fail for this token
- Token is automatically removed from storage after TTL expires
Storage Considerations:
- Tokens are stored with TTL matching their remaining lifetime
- After natural expiration, revoked tokens are cleaned up
- Only the token hash is stored (not the full token)
- Cleanup runs automatically at CleanupInterval
Parameters:
- ctx: Context for cancellation and timeout
- token: The JWT token string to revoke
Returns:
- error: If revocation is disabled, token is invalid, repository operation fails, or context is cancelled
Use Cases:
- User logout (revoke current access token)
- Security breach (revoke compromised token)
- Administrative action (revoke specific user's tokens)
- Password change (revoke all existing tokens)
Limitations:
- Requires RevocationEnabled = true
- Requires TokenRepository
- Adds latency to token verification (repository lookup)
- Requires distributed storage for multi-instance systems
Example (Logout - revoke access token):
err := maker.RevokeAccessToken(ctx, accessTokenString)
if err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
// Token is now invalid and will fail verification
fmt.Println("User logged out successfully")
Example (Revoke all session tokens):
// Get all tokens for a session from your database
tokens, err := db.GetSessionTokens(ctx, sessionID)
if err != nil {
return err
}
// Revoke each token
for _, token := range tokens {
if err := maker.RevokeAccessToken(ctx, token); err != nil {
log.Printf("Failed to revoke token: %v", err)
}
}
Example (Security breach response):
// Revoke access token immediately
if err := maker.RevokeAccessToken(ctx, compromisedToken); err != nil {
log.Printf("Failed to revoke compromised token: %v", err)
}
// Also revoke refresh token to prevent new access tokens
if err := maker.RevokeRefreshToken(ctx, refreshToken); err != nil {
log.Printf("Failed to revoke refresh token: %v", err)
}
func (*JWTMaker) RevokeRefreshToken ยถ added in v1.0.2
RevokeRefreshToken marks a refresh token as revoked, preventing its further use. The token cannot be used to obtain new access tokens after revocation. Requires RevocationEnabled to be true in the configuration.
Revocation Process:
- Parses the token to extract expiration time
- Calculates time-to-live (TTL) until natural expiration
- Stores token hash in repository with TTL
- Token verification will now fail for this token
- Token is automatically removed from storage after TTL expires
Impact:
- Prevents token from being used in RotateRefreshToken
- Prevents token from being verified successfully
- Existing access tokens remain valid until they expire
- User must re-authenticate to get new refresh token
Parameters:
- ctx: Context for cancellation and timeout
- token: The JWT refresh token string to revoke
Returns:
- error: If revocation is disabled, token is invalid, repository operation fails, or context is cancelled
Use Cases:
- User logout (revoke refresh token to prevent new access tokens)
- Forced re-authentication (after password change)
- Security incident response
- Session termination
Best Practices:
- Always revoke both access and refresh tokens during logout
- Revoke refresh tokens after password changes
- Monitor failed verification attempts (may indicate stolen tokens)
- Use short-lived refresh tokens if revocation is disabled
Example (Complete logout):
// Revoke both access and refresh tokens
if err := maker.RevokeAccessToken(ctx, accessToken); err != nil {
log.Printf("Failed to revoke access token: %v", err)
}
if err := maker.RevokeRefreshToken(ctx, refreshToken); err != nil {
return fmt.Errorf("failed to revoke refresh token: %w", err)
}
fmt.Println("Logged out successfully")
Example (Password change - revoke all user tokens):
// Get all refresh tokens for user
tokens, err := db.GetUserRefreshTokens(ctx, userID)
if err != nil {
return err
}
// Revoke each refresh token
for _, token := range tokens {
if err := maker.RevokeRefreshToken(ctx, token); err != nil {
log.Printf("Failed to revoke token: %v", err)
}
}
// User must re-authenticate on all devices
func (*JWTMaker) RotateRefreshToken ยถ
func (maker *JWTMaker) RotateRefreshToken(ctx context.Context, oldToken string) (*RefreshTokenResponse, error)
RotateRefreshToken exchanges an old refresh token for a new one with extended expiration. The old token is atomically marked as rotated and cannot be reused, preventing token theft. Requires RotationEnabled to be true in the configuration.
Rotation Process:
- Verifies the old token is valid (signature, expiration, not revoked/rotated)
- Atomically marks the old token as rotated using compare-and-swap
- If already rotated (by another request), returns error
- Creates a new refresh token with the same user and session
- Returns the new token with fresh expiration time
Atomicity Guarantee:
Uses MarkTokenRotatedAtomic to ensure only ONE concurrent request succeeds. If multiple requests attempt to rotate the same token simultaneously, only the first succeeds and others receive an error. This detects token theft attempts.
Security Benefits:
- Prevents token reuse attacks
- Detects concurrent rotation attempts (possible token theft)
- Limits blast radius of compromised tokens
- Enforces one-time use of refresh tokens
Parameters:
- ctx: Context for cancellation and timeout
- oldToken: The current refresh token to rotate (will be invalidated)
Returns:
- *RefreshTokenResponse: New refresh token with extended expiration
- error: If rotation is disabled, old token is invalid/already rotated, atomic operation fails, or context is cancelled
Error Scenarios:
- Token already rotated: Possible replay attack, invalidate session
- Token invalid/expired: Require re-authentication
- Rotation disabled: Feature not configured
- Repository error: Storage system unavailable
Security Implications:
If rotation fails due to "already rotated": - Indicates possible token theft - Consider invalidating the entire session - Alert security monitoring systems - Require full re-authentication
Example (Token refresh endpoint):
func refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
// Get refresh token from cookie or body
oldToken := r.Header.Get("X-Refresh-Token")
// Rotate the token
newRefresh, err := maker.RotateRefreshToken(r.Context(), oldToken)
if err != nil {
if strings.Contains(err.Error(), "rotated") {
// Possible token theft detected
log.Printf("Token reuse detected for token: %v", err)
http.Error(w, "security violation", http.StatusForbidden)
return
}
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// Create new access token
claims, _ := maker.VerifyRefreshToken(r.Context(), newRefresh.Token)
accessToken, err := maker.CreateAccessToken(
r.Context(),
claims.Subject,
claims.Username,
getUserRoles(claims.Subject), // Load from DB
claims.SessionID,
)
// Return new token pair
json.NewEncoder(w).Encode(map[string]string{
"access_token": accessToken.Token,
"refresh_token": newRefresh.Token,
})
}
Example (With security monitoring):
newToken, err := maker.RotateRefreshToken(ctx, oldToken)
if err != nil {
if strings.Contains(err.Error(), "rotated") {
// Token theft detected
securityLog.Alert("Token reuse attempt detected", map[string]interface{}{
"token_prefix": oldToken[:10],
"ip_address": clientIP,
"timestamp": time.Now(),
})
// Revoke all user tokens
revokeAllUserTokens(ctx, userID)
return nil, fmt.Errorf("security violation: session terminated")
}
return nil, err
}
Example (Race condition handling):
// Multiple concurrent requests with same token
var wg sync.WaitGroup
results := make(chan error, 3)
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := maker.RotateRefreshToken(ctx, sameToken)
results <- err
}()
}
wg.Wait()
close(results)
// Only one should succeed, others should get "already rotated" error
successCount := 0
for err := range results {
if err == nil {
successCount++
}
}
// successCount should be exactly 1
func (*JWTMaker) VerifyAccessToken ยถ
func (maker *JWTMaker) VerifyAccessToken(ctx context.Context, tokenString string) (*AccessTokenClaims, error)
VerifyAccessToken validates an access token and returns its claims if valid. Performs comprehensive security checks including signature, expiration, and revocation.
Validation Steps:
- Check context cancellation
- Check revocation status (if RevocationEnabled)
- Verify cryptographic signature
- Validate token structure and algorithm
- Check all timestamps (iat, exp, nbf, mle)
- Verify required claims are present
- Validate token type is "access"
- Parse and return claims
Checks Performed:
- Signature matches expected algorithm
- Token has not expired (exp > now)
- Token is not used before valid time (nbf <= now)
- Token has not exceeded maximum lifetime (mle > now)
- Token has not been revoked (if revocation enabled)
- All required claims are present
- Token type is "access" (not "refresh")
Parameters:
- ctx: Context for cancellation. Checked multiple times during verification.
- tokenString: The JWT token string to verify (from Authorization header)
Returns:
- *AccessTokenClaims: Parsed and validated token claims with all user information
- error: If token is invalid, expired, revoked, or any validation check fails
Error Scenarios:
- Invalid signature: Token has been tampered with
- Expired: Token lifetime has ended
- Revoked: Token was explicitly invalidated
- Wrong type: Refresh token used where access token expected
- Missing claims: Token structure is incomplete
- Future issued: Token claims to be issued in the future
- Context cancelled: Operation was cancelled by caller
Example (Verify and use claims):
claims, err := maker.VerifyAccessToken(ctx, tokenString)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
// Use claims for authorization
if !hasRole(claims.Roles, "admin") {
return fmt.Errorf("insufficient permissions")
}
// Access user information
fmt.Printf("User: %s (%s)\n", claims.Username, claims.Subject)
fmt.Printf("Session: %s\n", claims.SessionID)
fmt.Printf("Roles: %v\n", claims.Roles)
Example (HTTP middleware):
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing authorization", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := maker.VerifyAccessToken(r.Context(), token)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// Add claims to context
ctx := context.WithValue(r.Context(), "user_claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (*JWTMaker) VerifyRefreshToken ยถ
func (maker *JWTMaker) VerifyRefreshToken(ctx context.Context, tokenString string) (*RefreshTokenClaims, error)
VerifyRefreshToken validates a refresh token and returns its claims if valid. Performs comprehensive security checks including signature, expiration, revocation, and rotation.
Validation Steps:
- Check context cancellation
- Check revocation status (if RevocationEnabled)
- Check rotation status (if RotationEnabled)
- Verify cryptographic signature
- Validate token structure and algorithm
- Check all timestamps (iat, exp, nbf, mle)
- Verify required claims are present
- Validate token type is "refresh"
- Parse and return claims
Checks Performed:
- Signature matches expected algorithm
- Token has not expired (exp > now)
- Token is not used before valid time (nbf <= now)
- Token has not exceeded maximum lifetime (mle > now)
- Token has not been revoked (if revocation enabled)
- Token has not been rotated (if rotation enabled)
- All required claims are present
- Token type is "refresh" (not "access")
Parameters:
- ctx: Context for cancellation
- tokenString: The JWT refresh token string to verify
Returns:
- *RefreshTokenClaims: Parsed and validated token claims
- error: If token is invalid, expired, revoked, rotated, or any validation check fails
Error Scenarios:
- Invalid signature: Token has been tampered with
- Expired: Token lifetime has ended
- Revoked: Token was explicitly invalidated
- Rotated: Token has been used to obtain a new token (rotation enabled)
- Wrong type: Access token used where refresh token expected
- Missing claims: Token structure is incomplete
- Context cancelled: Operation was cancelled
Use Cases:
- Validating refresh token before rotation
- Checking refresh token validity before issuing new access token
- Verifying refresh token in logout operations
Example (Refresh access token):
// Verify the refresh token
refreshClaims, err := maker.VerifyRefreshToken(ctx, refreshTokenString)
if err != nil {
return nil, fmt.Errorf("invalid refresh token: %w", err)
}
// Create new access token with same user/session
newAccessToken, err := maker.CreateAccessToken(
ctx,
refreshClaims.Subject,
refreshClaims.Username,
[]string{"user"}, // Load roles from database
refreshClaims.SessionID,
)
Example (With rotation):
// Automatically rotates the token if rotation is enabled
newRefreshToken, err := maker.RotateRefreshToken(ctx, oldRefreshTokenString)
if err != nil {
return fmt.Errorf("rotation failed: %w", err)
}
// Old token is now invalid, use new token
type MemoryTokenRepository ยถ added in v1.0.6
type MemoryTokenRepository struct {
// contains filtered or unexported fields
}
MemoryTokenRepository is an in-memory implementation of TokenRepository. Suitable for development, testing, or single-instance deployments.
Architecture Characteristics:
- Fully concurrent-safe with sync.RWMutex
- Automatic background cleanup of expired entries
- Three separate maps for different token types
- Memory-efficient storage using token hashes
Performance Characteristics:
- O(1) average case for lookups and inserts
- Read-preferring RWLock for high concurrency
- Background cleanup minimizes main operation impact
- No network latency (pure in-memory operations)
Memory Usage Considerations:
- Memory grows with number of active tokens
- Automatic cleanup prevents unbounded growth
- Each token entry consumes ~88 bytes + map overhead
- Typical usage: thousands to tens of thousands of tokens
Limitations:
- Data lost on application restart
- Not suitable for distributed systems
- Memory consumption proportional to active tokens
- No persistence for audit requirements
func (*MemoryTokenRepository) CleanupExpiredRevokedTokens ยถ added in v1.0.6
func (m *MemoryTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
CleanupExpiredRevokedTokens removes expired revoked tokens from memory. Thread-safe operation with write lock protection.
Cleanup Strategy:
- Iterates through specified token type map
- Removes entries where expiration time has passed
- O(n) operation where n is number of entries for that type
- Run periodically to prevent memory leaks
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- tokenType: Type of tokens to cleanup (AccessToken or RefreshToken)
Returns:
- error: If token type is invalid
Example (Manual cleanup):
err := repo.CleanupExpiredRevokedTokens(ctx, AccessToken)
if err != nil {
log.Printf("Failed to cleanup access tokens: %v", err)
}
func (*MemoryTokenRepository) CleanupExpiredRotatedTokens ยถ added in v1.0.6
func (m *MemoryTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
CleanupExpiredRotatedTokens removes expired rotated tokens from memory. Thread-safe operation with write lock protection.
Cleanup Efficiency:
- Single pass through rotated tokens map
- O(n) operation where n is number of rotated tokens
- Typically fewer rotated tokens than revoked tokens
- Helps prevent memory bloat
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
Returns:
- error: Always nil (error included for interface compatibility)
Example (Manual cleanup):
err := repo.CleanupExpiredRotatedTokens(ctx)
if err != nil {
log.Printf("Failed to cleanup rotated tokens: %v", err)
}
func (*MemoryTokenRepository) Close ยถ added in v1.0.6
func (m *MemoryTokenRepository) Close() error
Close stops the background cleanup goroutine. Implements graceful shutdown pattern to prevent goroutine leaks.
Important: Call this method when shutting down the application to ensure proper cleanup of resources and prevent goroutine leaks.
Implementation Details:
- Uses sync.Once to ensure idempotent operation
- Closes stopCleanup channel to signal goroutine termination
- Goroutine will complete current cleanup cycle before exiting
- Safe to call multiple times
Returns:
- error: Always nil (error included for interface compatibility)
Example (Application shutdown):
// Handle graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Close repository
if err := repo.Close(); err != nil {
log.Printf("Warning: failed to close token repository: %v", err)
}
func (*MemoryTokenRepository) GetRotationTTL ยถ added in v1.0.6
func (m *MemoryTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
GetRotationTTL returns the remaining TTL for a rotated token. Useful for debugging and monitoring rotation patterns.
Use Cases:
- Debugging rotation-related issues
- Monitoring token lifecycle
- Understanding cleanup timing
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- token: The token to check for remaining TTL
Returns:
- time.Duration: Remaining TTL if token is rotated and not expired, 0 otherwise
- error: If token is empty
Example (Monitor rotation TTL):
ttl, err := repo.GetRotationTTL(ctx, "rotated-token")
if err != nil {
return fmt.Errorf("failed to get TTL: %w", err)
}
log.Printf("Token will be automatically cleaned up in %v", ttl)
func (*MemoryTokenRepository) IsTokenRevoked ยถ added in v1.0.6
func (m *MemoryTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
IsTokenRevoked checks if a token has been revoked by checking its hash in memory. Thread-safe operation with read lock for high concurrency.
Performance Characteristics:
- O(1) average case lookup
- Read lock enables concurrent reads
- Automatic expiration check during lookup
- No network or disk I/O
Operation Sequence:
- Hash the provided token
- Acquire read lock
- Lookup in appropriate token map
- Check expiration time
- Return revocation status
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- tokenType: Type of token to check (AccessToken or RefreshToken)
- token: The token string to check for revocation
Returns:
- bool: True if token is revoked and not expired, false otherwise
- error: If token is empty or token type is invalid
Example (Check access token):
revoked, err := repo.IsTokenRevoked(ctx, AccessToken, "jwt-token-here")
if err != nil {
return fmt.Errorf("failed to check revocation: %w", err)
}
if revoked {
return errors.New("token has been revoked")
}
func (*MemoryTokenRepository) IsTokenRotated ยถ added in v1.0.6
IsTokenRotated checks if a token has been rotated by checking its hash in memory. Thread-safe operation with read lock for high concurrency.
Security Purpose:
- Detects if a refresh token has been previously rotated
- Prevents reuse of rotated tokens
- Essential for rotation-based security
Performance:
- O(1) average case lookup
- Read lock enables concurrent access
- Automatic expiration check
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- token: The token to check for rotation status
Returns:
- bool: True if token has been rotated and not expired, false otherwise
- error: If token is empty
Example (Check rotation status):
rotated, err := repo.IsTokenRotated(ctx, "suspect-token")
if err != nil {
return fmt.Errorf("failed to check rotation: %w", err)
}
if rotated {
return errors.New("token has been rotated - do not accept")
}
func (*MemoryTokenRepository) MarkTokenRevoke ยถ added in v1.0.6
func (m *MemoryTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
MarkTokenRevoke marks a token as revoked by storing its hash in memory. Thread-safe operation with write lock protection.
Memory Storage:
- Token hash stored in type-specific map
- Expiration time stored for automatic cleanup
- Separate maps for access vs refresh tokens
- O(1) insertion time with map store
Security Implementation:
- Only SHA-256 hash of token stored
- No sensitive token data retained
- Automatic expiration via TTL
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- tokenType: Type of token (AccessToken or RefreshToken)
- token: The actual token string to revoke
- ttl: Time-to-live duration for the revocation record
Returns:
- error: If token is empty, TTL is invalid, or token type is invalid
Example (Revoke access token):
err := repo.MarkTokenRevoke(ctx, AccessToken, "jwt-token-here", 15*time.Minute)
if err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
Example (Revoke refresh token):
err := repo.MarkTokenRevoke(ctx, RefreshToken, "refresh-token-here", 7*24*time.Hour)
if err != nil {
return fmt.Errorf("failed to revoke refresh token: %w", err)
}
func (*MemoryTokenRepository) MarkTokenRotated ยถ added in v1.0.6
func (m *MemoryTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
MarkTokenRotated marks a token as rotated by storing its hash in memory. Thread-safe operation with write lock protection.
Security Purpose:
- Prevents replay of rotated refresh tokens
- Essential for rotation-based security
- Protects against token reuse attacks
Memory Storage:
- Single map for all rotated tokens
- O(1) insertion time
- Automatic expiration via TTL
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- error: If token is empty or TTL is invalid
Example (Mark token as rotated):
err := repo.MarkTokenRotated(ctx, "old-refresh-token", 24*time.Hour)
if err != nil {
return fmt.Errorf("failed to mark token as rotated: %w", err)
}
func (*MemoryTokenRepository) MarkTokenRotatedAtomic ยถ added in v1.0.6
func (m *MemoryTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
MarkTokenRotatedAtomic marks a token as rotated atomically, returning whether it was newly rotated. Provides true atomicity for rotation detection in concurrent scenarios.
Key Features:
- Atomic check-and-set operation
- Returns whether rotation was actually performed
- Essential for preventing double-spending in rotation flows
- Thread-safe with proper locking
Difference from MarkTokenRotated:
- Returns boolean indicating if rotation was new
- Performs existence check within critical section
- Prevents race conditions in concurrent environments
Parameters:
- ctx: Context for cancellation (not used in memory implementation)
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- bool: True if token was newly rotated, false if already rotated
- error: If token is empty or TTL is invalid
Example (Atomic rotation check):
rotated, err := repo.MarkTokenRotatedAtomic(ctx, "refresh-token", time.Hour)
if err != nil {
return fmt.Errorf("rotation failed: %w", err)
}
if !rotated {
return errors.New("token was already rotated - potential security issue")
}
// Safe to proceed with new token issuance
func (*MemoryTokenRepository) Stats ยถ added in v1.0.6
func (m *MemoryTokenRepository) Stats() map[string]int
Stats returns statistics about the repository for monitoring and debugging. Provides snapshot of current memory usage and token counts.
Metrics Provided:
- revoked_access_tokens: Count of currently revoked access tokens
- revoked_refresh_tokens: Count of currently revoked refresh tokens
- rotated_tokens: Count of currently rotated tokens
Use Cases:
- Monitoring memory usage patterns
- Debugging token-related issues
- Capacity planning and performance analysis
- Health checking and alerting
Returns:
- map[string]int: Dictionary of current repository statistics
Example (Monitoring endpoint):
// Expose stats via HTTP endpoint
http.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
stats := repo.Stats()
json.NewEncoder(w).Encode(stats)
})
Example (Logging statistics):
stats := repo.Stats()
log.Printf("Token repository stats: %+v", stats)
type MongoTokenRepository ยถ added in v1.0.6
type MongoTokenRepository struct {
// contains filtered or unexported fields
}
MongoTokenRepository implements TokenRepository using MongoDB. This repository provides document-based storage for token revocation and rotation data.
Architecture Features:
- Document-oriented storage with flexible schema
- Automatic TTL-based expiration via MongoDB indexes
- Transaction support for multi-operation consistency
- Composite indexes for optimal query performance
- Connection pooling via MongoDB driver
Performance Characteristics:
- Sub-millisecond reads with proper indexing
- Efficient bulk operations for cleanup
- Horizontal scaling via MongoDB sharding
- Built-in replication for high availability
MongoDB Version Requirements:
- MongoDB 4.0+ for transaction support (recommended 4.2+)
- TTL index support (available since MongoDB 2.2)
- Composite index support for unique constraints
Production Considerations:
- Configure proper replica sets for production
- Set appropriate read/write concerns
- Monitor collection sizes and index usage
- Consider sharding for very high throughput
- Implement connection string with retry logic
func (*MongoTokenRepository) CleanupExpiredRevokedTokens ยถ added in v1.0.6
func (r *MongoTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
CleanupExpiredRevokedTokens removes expired revoked tokens from MongoDB. Note: MongoDB TTL indexes handle automatic cleanup, but this provides manual control.
Use Cases:
- Force immediate cleanup of expired documents
- Handle cases where TTL monitor is delayed
- Bulk cleanup during maintenance windows
- Testing and development environments
Performance Characteristics:
- DeleteMany operation for batch processing
- Index on expires_at enables efficient range query
- Transaction wrapper ensures operation consistency
- Batch operation minimizes database round trips
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of tokens to cleanup (AccessToken or RefreshToken)
Returns:
- error: If token type is invalid or database operation fails
Example (Manual cleanup):
err := repo.CleanupExpiredRevokedTokens(ctx, AccessToken)
if err != nil {
log.Printf("Failed to cleanup access tokens: %v", err)
}
MongoDB Operation:
db.revoked_tokens.deleteMany({
token_type: "access",
expires_at: { $lte: ISODate() }
})
func (*MongoTokenRepository) CleanupExpiredRotatedTokens ยถ added in v1.0.6
func (r *MongoTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
CleanupExpiredRotatedTokens removes expired rotated tokens from MongoDB. Provides manual control over TTL-based automatic cleanup.
Cleanup Strategy:
- DeleteMany operation with expires_at filter
- Leverages TTL index for efficient query
- Transaction wrapper for consistency
- Batch operation for efficiency
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- error: If database operation fails
Example (Scheduled manual cleanup):
// Run additional cleanup daily (TTL indexes handle most cases)
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
err := repo.CleanupExpiredRotatedTokens(ctx)
cancel()
if err != nil {
log.Printf("Manual rotated token cleanup failed: %v", err)
}
}
MongoDB Operation:
db.rotated_tokens.deleteMany({
expires_at: { $lte: ISODate() }
})
func (*MongoTokenRepository) Close ยถ added in v1.0.6
func (r *MongoTokenRepository) Close(ctx context.Context) error
Close performs cleanup operations for the MongoDB repository. Note: MongoDB driver manages connection pooling automatically.
Cleanup Behavior:
- MongoDB connections are managed by the driver
- No explicit connection closing needed for collections
- Client should be closed at application level
- This method exists for interface compatibility
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- error: Always nil (included for interface compatibility)
Important: While this repository doesn't require explicit closing, the MongoDB client should be closed during application shutdown:
defer func() {
if err := client.Disconnect(ctx); err != nil {
log.Printf("Failed to disconnect MongoDB: %v", err)
}
}()
func (*MongoTokenRepository) GetRotationTTL ยถ added in v1.0.6
func (r *MongoTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
GetRotationTTL returns the remaining TTL for a rotated token. Useful for debugging, monitoring, and cache optimization.
Use Cases:
- Debugging rotation-related issues
- Monitoring token rotation patterns
- Optimizing cleanup scheduling
- Understanding token lifecycle
Parameters:
- ctx: Context for cancellation and timeout
- token: The token to check for remaining TTL
Returns:
- time.Duration: Remaining TTL if token is rotated and not expired, 0 otherwise
- error: If token is empty or database operation fails
Example (Monitor rotation TTL):
ttl, err := repo.GetRotationTTL(ctx, "rotated-token")
if err != nil {
return fmt.Errorf("failed to get rotation TTL: %w", err)
}
if ttl > 0 {
log.Printf("Token will be automatically cleaned up in %v", ttl)
}
MongoDB Query:
db.rotated_tokens.findOne({ token_hash: "hash" })
func (*MongoTokenRepository) IsTokenRevoked ยถ added in v1.0.6
func (r *MongoTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
IsTokenRevoked checks if a token has been revoked by checking its hash in MongoDB. Performs efficient document lookup using composite index and expiration filter.
Query Optimization:
- Uses composite index (token_hash, token_type) for fast lookups
- Additional filter on expires_at leverages TTL index
- Single document read operation
- FindOne returns at most one document
Performance Characteristics:
- O(1) average case with proper indexing
- Index-only query possible with covered indexes
- Read operation with minimal locking
- No transaction overhead for reads
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of token to check (AccessToken or RefreshToken)
- token: The token string to check for revocation
Returns:
- bool: True if token is revoked and not expired, false otherwise
- error: If token is empty, token type is invalid, or database operation fails
Example (Check access token):
revoked, err := repo.IsTokenRevoked(ctx, AccessToken, "jwt-token-here")
if err != nil {
return fmt.Errorf("failed to check revocation: %w", err)
}
if revoked {
return errors.New("token has been revoked")
}
MongoDB Query:
db.revoked_tokens.findOne({
token_hash: "hash",
token_type: "access",
expires_at: { $gt: ISODate() }
})
func (*MongoTokenRepository) IsTokenRotated ยถ added in v1.0.6
IsTokenRotated checks if a token has been rotated by checking its hash in MongoDB. Performs efficient document lookup with automatic expiration filtering.
Security Purpose:
- Detects if a refresh token has been previously rotated
- Prevents reuse of rotated tokens in replay attacks
- Essential for rotation-based security schemes
Query Optimization:
- Uses unique index on token_hash for fast lookups
- Additional filter on expires_at leverages TTL index
- Single document read operation
Parameters:
- ctx: Context for cancellation and timeout
- token: The token to check for rotation status
Returns:
- bool: True if token has been rotated and not expired, false otherwise
- error: If token is empty or database operation fails
Example (Check rotation status):
rotated, err := repo.IsTokenRotated(ctx, "suspect-refresh-token")
if err != nil {
return fmt.Errorf("failed to check rotation: %w", err)
}
if rotated {
return errors.New("token has been rotated - do not accept")
}
MongoDB Query:
db.rotated_tokens.findOne({
token_hash: "hash",
expires_at: { $gt: ISODate() }
})
func (*MongoTokenRepository) MarkTokenRevoke ยถ added in v1.0.6
func (r *MongoTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
MarkTokenRevoke marks a token as revoked by storing its hash in MongoDB. Uses composite unique key (token_hash, token_type) for proper upsert behavior.
MongoDB Operation:
- Uses ReplaceOne with upsert for atomic create-or-update
- Composite unique index prevents duplicate entries
- Transaction wrapper ensures operation consistency
- Automatic TTL via index handles expiration
Performance Characteristics:
- Single document write operation
- Indexed fields enable fast conflict detection
- Upsert operation is atomic at document level
- Transaction overhead if enabled
Security Implementation:
- Only token hash stored, never actual token
- Composite key ensures type-specific revocation
- Automatic expiration prevents permanent storage
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of token (AccessToken or RefreshToken)
- token: The actual token string to revoke
- ttl: Time-to-live duration for the revocation record
Returns:
- error: If token is empty, TTL is invalid, token type is invalid, or database operation fails
Example (Revoke access token):
err := repo.MarkTokenRevoke(ctx, AccessToken, "jwt-token-here", 15*time.Minute)
if err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
MongoDB Query:
db.revoked_tokens.replaceOne(
{ token_hash: "hash", token_type: "access" },
{ token_hash: "hash", token_type: "access", expires_at: ISODate(), created_at: ISODate() },
{ upsert: true }
)
func (*MongoTokenRepository) MarkTokenRotated ยถ added in v1.0.6
func (r *MongoTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
MarkTokenRotated marks a token as rotated by storing its hash in MongoDB. Uses upsert operation for idempotent rotation tracking.
Security Purpose:
- Prevents replay of rotated refresh tokens
- Enables one-time use during token rotation flow
- Protects against token replay attacks
MongoDB Operation:
- ReplaceOne with upsert on token_hash field
- Unique index prevents duplicate rotations
- Transaction wrapper ensures operation consistency
- Automatic TTL via index handles expiration
Parameters:
- ctx: Context for cancellation and timeout
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- error: If token is empty, TTL is invalid, or database operation fails
Example (Mark token as rotated):
err := repo.MarkTokenRotated(ctx, "old-refresh-token", 7*24*time.Hour)
if err != nil {
return fmt.Errorf("failed to mark token as rotated: %w", err)
}
MongoDB Query:
db.rotated_tokens.replaceOne(
{ token_hash: "hash" },
{ token_hash: "hash", expires_at: ISODate(), created_at: ISODate() },
{ upsert: true }
)
func (*MongoTokenRepository) MarkTokenRotatedAtomic ยถ added in v1.0.6
func (r *MongoTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
MarkTokenRotatedAtomic marks a token as rotated atomically, returning whether it was newly rotated. Uses InsertOne instead of ReplaceOne to detect true first-time rotation.
Key Difference from MarkTokenRotated:
- Returns boolean indicating if rotation was actually performed
- Uses InsertOne which fails on duplicate key (already rotated)
- Essential for preventing double-spending in rotation flows
- Provides true atomicity for rotation detection
Use Cases:
- Preventing race conditions during concurrent token rotation
- Ensuring exactly-once rotation semantics
- Distributed system environments with multiple validators
Parameters:
- ctx: Context for cancellation and timeout
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- bool: True if token was newly rotated, false if already rotated
- error: If token is empty, TTL is invalid, or database operation fails
Example (Atomic rotation check):
rotated, err := repo.MarkTokenRotatedAtomic(ctx, "refresh-token", 24*time.Hour)
if err != nil {
return fmt.Errorf("rotation failed: %w", err)
}
if !rotated {
return errors.New("token was already rotated - potential replay attack")
}
// Proceed with issuing new tokens
MongoDB Operation:
db.rotated_tokens.insertOne({
token_hash: "hash",
expires_at: ISODate(),
created_at: ISODate()
})
// Fails with duplicate key error if already exists
func (*MongoTokenRepository) Stats ยถ added in v1.0.6
func (r *MongoTokenRepository) Stats(ctx context.Context) (map[string]interface{}, error)
Stats returns statistics about the repository for monitoring and debugging. Provides insights into token revocation and rotation patterns.
Metrics Collected:
- Total revoked tokens (all types)
- Revoked access tokens count
- Revoked refresh tokens count
- Rotated tokens count
Performance Characteristics:
- CountDocuments operations use collection statistics
- Efficient counting with MongoDB's metadata
- Separate queries for different token types
- No document scanning for count operations
Use Cases:
- Monitoring system health and usage patterns
- Capacity planning and performance tuning
- Security auditing and anomaly detection
- Debugging token-related issues
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- map[string]interface{}: Dictionary of statistics metrics
- error: If any database count operation fails
Example (Prometheus metrics export):
stats, err := repo.Stats(ctx)
if err != nil {
log.Printf("Failed to get stats: %v", err)
return
}
revokedTotal.Set(float64(stats["total_revoked_tokens"].(int64)))
rotatedCount.Set(float64(stats["rotated_tokens"].(int64)))
type RedisTokenRepository ยถ added in v1.0.6
type RedisTokenRepository struct {
// contains filtered or unexported fields
}
RedisTokenRepository implements TokenRepository using Redis. This repository provides high-performance in-memory storage for token revocation and rotation data.
Architecture Features:
- Key-value storage with TTL-based expiration
- Atomic operations using Redis commands
- Pipeline support for batch operations
- Automatic key expiration via Redis TTL
- Connection pooling via Redis client
Performance Characteristics:
- Sub-millisecond read/write operations
- O(1) average case for all operations
- In-memory performance with optional persistence
- Horizontal scaling via Redis Cluster
Redis Features Utilized:
- SET with EX/PX for TTL-based expiration
- SETNX for atomic insert-if-not-exists
- GET for fast lookups
- PTTL for precise TTL checking
- SCAN for efficient key enumeration
- Pipeline for batch operations
Memory Usage Considerations:
- Each key consumes memory based on prefix + hash (typically ~100 bytes)
- TTL ensures automatic memory reclamation
- Redis compression can reduce memory usage
- Consider maxmemory policy for production
func (*RedisTokenRepository) CleanupExpiredRevokedTokens ยถ added in v1.0.6
func (r *RedisTokenRepository) CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
CleanupExpiredRevokedTokens removes expired revoked tokens from Redis. Note: Redis automatically removes expired keys, but this provides manual control.
Use Cases:
- Force immediate cleanup of expired keys
- Handle memory fragmentation issues
- Bulk cleanup during maintenance windows
- Testing and development environments
Cleanup Strategy:
- SCAN iterates through keys with specified prefix
- PTTL checks remaining TTL for each key
- DEL removes keys that are expired or have no TTL
- Batch processing for memory efficiency
Performance Characteristics:
- O(N) where N is number of keys with prefix
- SCAN is non-blocking and production-safe
- Pipeline reduces round trips
- Batch deletion improves efficiency
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of tokens to cleanup (AccessToken or RefreshToken)
Returns:
- error: If token type is invalid or Redis operation fails
Example (Manual cleanup):
err := repo.CleanupExpiredRevokedTokens(ctx, AccessToken)
if err != nil {
log.Printf("Failed to cleanup access tokens: %v", err)
}
func (*RedisTokenRepository) CleanupExpiredRotatedTokens ยถ added in v1.0.6
func (r *RedisTokenRepository) CleanupExpiredRotatedTokens(ctx context.Context) error
CleanupExpiredRotatedTokens removes expired rotated tokens from Redis. Provides manual control over Redis's automatic key expiration.
Cleanup Strategy:
- SCAN iterates through rotated token keys
- PTTL checks remaining TTL for each key
- DEL removes expired keys in batches
- Context cancellation support for long-running operations
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- error: If Redis operation fails
Example (Scheduled manual cleanup):
// Run additional cleanup weekly (Redis TTL handles most cases)
ticker := time.NewTicker(7 * 24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
err := repo.CleanupExpiredRotatedTokens(ctx)
cancel()
if err != nil {
log.Printf("Manual rotated token cleanup failed: %v", err)
}
}
func (*RedisTokenRepository) Close ยถ added in v1.0.6
func (r *RedisTokenRepository) Close() error
Close performs cleanup operations and closes the Redis connection. Implements graceful shutdown pattern for resource cleanup.
Cleanup Actions:
- Closes Redis client connection pool
- Releases all Redis connections
- Prevents connection leaks during application shutdown
Important: This should be called during application shutdown to prevent Redis connection leaks and ensure graceful termination.
Returns:
- error: If closing the Redis client fails
Example (Graceful shutdown):
// Setup signal handling
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Wait for shutdown signal
<-ctx.Done()
// Close repository during shutdown
if err := repo.Close(); err != nil {
log.Printf("Failed to close Redis token repository: %v", err)
}
func (*RedisTokenRepository) GetRotationTTL ยถ added in v1.0.6
func (r *RedisTokenRepository) GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
GetRotationTTL returns the remaining TTL for a rotated token. Uses PTTL for millisecond precision TTL checking.
Use Cases:
- Debugging rotation-related issues
- Monitoring token rotation patterns
- Optimizing cleanup scheduling
- Understanding token lifecycle
Redis PTTL Behavior:
- Returns -2 if key doesn't exist
- Returns -1 if key exists but has no expiry
- Returns positive value for remaining TTL in milliseconds
- Millisecond precision for accurate timing
Parameters:
- ctx: Context for cancellation and timeout
- token: The token to check for remaining TTL
Returns:
- time.Duration: Remaining TTL if token is rotated and not expired, 0 otherwise
- error: If token is empty or Redis operation fails
Example (Monitor rotation TTL):
ttl, err := repo.GetRotationTTL(ctx, "rotated-token")
if err != nil {
return fmt.Errorf("failed to get rotation TTL: %w", err)
}
if ttl > 0 {
log.Printf("Token will be automatically cleaned up in %v", ttl)
}
Redis Command:
PTTL rotated:token_hash
func (*RedisTokenRepository) IsTokenRevoked ยถ added in v1.0.6
func (r *RedisTokenRepository) IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
IsTokenRevoked checks if a token has been revoked by checking its hash in Redis. Uses GET operation for reliable existence checking with TTL validation.
Redis Operation:
- GET key
- Returns value if key exists and TTL hasn't expired
- Returns nil if key doesn't exist or TTL has expired
Performance Characteristics:
- O(1) time complexity
- Single GET operation
- In-memory lookup for maximum speed
- Automatic TTL handling by Redis
Key Advantages over EXISTS:
- GET is atomic and reliable
- Works correctly with expiring keys
- Returns actual value for verification
- Consistent behavior in cluster environments
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of token to check (AccessToken or RefreshToken)
- token: The token string to check for revocation
Returns:
- bool: True if token is revoked and not expired, false otherwise
- error: If token is empty, token type is invalid, or Redis operation fails
Example (Check access token):
revoked, err := repo.IsTokenRevoked(ctx, AccessToken, "jwt-token-here")
if err != nil {
return fmt.Errorf("failed to check revocation: %w", err)
}
if revoked {
return errors.New("token has been revoked")
}
Redis Command:
GET revoked:access:token_hash
func (*RedisTokenRepository) IsTokenRotated ยถ added in v1.0.6
IsTokenRotated checks if a token has been rotated by checking its hash in Redis. Uses GET operation for reliable existence checking with TTL validation.
Security Purpose:
- Detects if a refresh token has been previously rotated
- Prevents reuse of rotated tokens in replay attacks
- Essential for rotation-based security schemes
Performance Characteristics:
- O(1) time complexity
- Single GET operation
- In-memory lookup for maximum speed
- Automatic TTL handling by Redis
Parameters:
- ctx: Context for cancellation and timeout
- token: The token to check for rotation status
Returns:
- bool: True if token has been rotated and not expired, false otherwise
- error: If token is empty or Redis operation fails
Example (Check rotation status):
rotated, err := repo.IsTokenRotated(ctx, "suspect-refresh-token")
if err != nil {
return fmt.Errorf("failed to check rotation: %w", err)
}
if rotated {
return errors.New("token has been rotated - do not accept")
}
Redis Command:
GET rotated:token_hash
func (*RedisTokenRepository) MarkTokenRevoke ยถ added in v1.0.6
func (r *RedisTokenRepository) MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
MarkTokenRevoke marks a token as revoked by storing its hash in Redis. Uses key with TTL for automatic expiration and pipeline for atomic operation.
Redis Operation:
- SET key value EX seconds
- Key format: "revoked:access:{hash}" or "revoked:refresh:{hash}"
- Value: "1" (simple marker)
- TTL: Automatic expiration after specified duration
Performance Characteristics:
- Single SET operation with TTL
- O(1) time complexity
- Pipeline ensures atomic execution
- In-memory operation for maximum speed
Security Implementation:
- Only token hash stored, never actual token
- Automatic expiration via TTL
- Separate namespaces for access vs refresh tokens
Parameters:
- ctx: Context for cancellation and timeout
- tokenType: Type of token (AccessToken or RefreshToken)
- token: The actual token string to revoke
- ttl: Time-to-live duration for the revocation record
Returns:
- error: If token is empty, TTL is invalid, token type is invalid, or Redis operation fails
Example (Revoke access token):
err := repo.MarkTokenRevoke(ctx, AccessToken, "jwt-token-here", 15*time.Minute)
if err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
Redis Command:
SET revoked:access:token_hash "1" EX 900
func (*RedisTokenRepository) MarkTokenRotated ยถ added in v1.0.6
func (r *RedisTokenRepository) MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
MarkTokenRotated marks a token as rotated by storing its hash in Redis. Uses key with TTL for automatic expiration and pipeline for atomic operation.
Security Purpose:
- Prevents replay of rotated refresh tokens
- Enables one-time use during token rotation flow
- Protects against token replay attacks
Redis Operation:
- SET key value EX seconds
- Key format: "rotated:{hash}"
- Value: "1" (simple marker)
- TTL: Automatic expiration after specified duration
Parameters:
- ctx: Context for cancellation and timeout
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- error: If token is empty, TTL is invalid, or Redis operation fails
Example (Mark token as rotated):
err := repo.MarkTokenRotated(ctx, "old-refresh-token", 7*24*time.Hour)
if err != nil {
return fmt.Errorf("failed to mark token as rotated: %w", err)
}
Redis Command:
SET rotated:token_hash "1" EX 604800
func (*RedisTokenRepository) MarkTokenRotatedAtomic ยถ added in v1.0.6
func (r *RedisTokenRepository) MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
MarkTokenRotatedAtomic marks a token as rotated atomically, returning whether it was newly rotated. Uses SETNX (SET if Not eXists) for true atomic rotation detection.
Key Difference from MarkTokenRotated:
- Returns boolean indicating if rotation was actually performed
- Uses SETNX which only sets if key doesn't exist
- Essential for preventing double-spending in rotation flows
- Provides true atomicity for rotation detection
Redis Operation:
- SETNX key value
- EXPIRE key seconds (if SETNX succeeds)
- Atomic check-and-set operation
Use Cases:
- Preventing race conditions during concurrent token rotation
- Ensuring exactly-once rotation semantics
- Distributed system environments with multiple validators
Parameters:
- ctx: Context for cancellation and timeout
- token: The refresh token being rotated
- ttl: Time-to-live duration for rotation record
Returns:
- bool: True if token was newly rotated, false if already rotated
- error: If token is empty, TTL is invalid, or Redis operation fails
Example (Atomic rotation check):
rotated, err := repo.MarkTokenRotatedAtomic(ctx, "refresh-token", 24*time.Hour)
if err != nil {
return fmt.Errorf("rotation failed: %w", err)
}
if !rotated {
return errors.New("token was already rotated - potential replay attack")
}
// Safe to proceed with new token issuance
Redis Commands:
SETNX rotated:token_hash "1" EXPIRE rotated:token_hash 86400
func (*RedisTokenRepository) Stats ยถ added in v1.0.6
func (r *RedisTokenRepository) Stats(ctx context.Context) (map[string]interface{}, error)
Stats returns statistics about the repository for monitoring and debugging. Provides insights into token revocation and rotation patterns using SCAN operations.
Metrics Collected:
- Total revoked tokens (all types)
- Revoked access tokens count
- Revoked refresh tokens count
- Rotated tokens count
Performance Characteristics:
- Uses SCAN for production-safe key counting
- O(N) where N is total number of keys
- Batch processing for memory efficiency
- Non-blocking operations
Use Cases:
- Monitoring system health and usage patterns
- Capacity planning and performance tuning
- Security auditing and anomaly detection
- Debugging token-related issues
Parameters:
- ctx: Context for cancellation and timeout
Returns:
- map[string]interface{}: Dictionary of statistics metrics
- error: If any Redis count operation fails
Example (Monitoring dashboard):
stats, err := repo.Stats(ctx)
if err != nil {
log.Printf("Failed to get Redis stats: %v", err)
return
}
fmt.Printf("Redis token statistics: %+v\n", stats)
type RefreshTokenClaims ยถ
type RefreshTokenClaims struct {
// ID is the unique token identifier (UUIDv4) used for tracking and rotation.
ID uuid.UUID `json:"jti"`
// Subject is the user's unique identifier (UUID).
Subject uuid.UUID `json:"sub"`
// SessionID uniquely identifies the user's session (UUIDv4).
SessionID uuid.UUID `json:"sid"`
// Username is the human-readable username.
Username string `json:"usr"`
// Issuer identifies the service that created this token.
Issuer string `json:"iss"`
// Audience lists the services that should accept this token.
Audience []string `json:"aud"`
// IssuedAt is the timestamp when this token was created (UTC).
IssuedAt time.Time `json:"iat"`
// ExpiresAt is the timestamp when this token expires (UTC).
ExpiresAt time.Time `json:"exp"`
// NotBefore is the optional timestamp before which the token is not valid (UTC).
NotBefore time.Time `json:"nbf"`
// MaxLifetimeExpiry is the absolute expiration time (RFC3339 format).
MaxLifetimeExpiry time.Time `json:"mle"`
// TokenType is always "refresh" for refresh tokens.
TokenType TokenType `json:"typ"`
}
RefreshTokenClaims represents the claims contained in a refresh token JWT. Refresh tokens are long-lived and used to obtain new access tokens without re-authentication.
Standard JWT Claims:
- jti: JWT ID (unique token identifier)
- sub: Subject (user UUID)
- iss: Issuer (authentication service)
- aud: Audience (intended recipients)
- iat: Issued At (token creation time)
- exp: Expiration Time
- nbf: Not Before (optional, token not valid before this time)
Custom Claims:
- sid: Session ID (for tracking user sessions)
- usr: Username (human-readable identifier)
- typ: Token Type (always "refresh" for refresh tokens)
- mle: Maximum Lifetime Expiry (absolute expiration)
Note: Refresh tokens do not include roles since they're only used to obtain new access tokens.
type RefreshTokenResponse ยถ
type RefreshTokenResponse struct {
// Subject is the user's unique identifier (UUID).
Subject uuid.UUID `json:"sub"`
// SessionID uniquely identifies the user's session (UUID).
SessionID uuid.UUID `json:"sid"`
// Token is the signed JWT string that can be used to obtain new access tokens.
Token string `json:"tok"`
// Issuer identifies the authentication service.
Issuer string `json:"iss"`
// Username is the human-readable username.
Username string `json:"usr"`
// Audience lists the intended recipients of this token.
Audience []string `json:"aud"`
// IssuedAt is when the token was created (RFC3339 format).
IssuedAt time.Time `json:"iat"`
// ExpiresAt is when the token expires (RFC3339 format).
ExpiresAt time.Time `json:"exp"`
// NotBefore is when the token becomes valid (RFC3339 format).
NotBefore time.Time `json:"nbf"`
// MaxLifetimeExpiry is the absolute expiration time (RFC3339 format).
MaxLifetimeExpiry time.Time `json:"mle"`
// TokenType is always "refresh".
TokenType TokenType `json:"typ"`
}
RefreshTokenResponse contains the generated refresh token and its associated metadata. This is returned after successful refresh token creation or rotation.
type RevokedTokenType ยถ added in v1.0.6
type RevokedTokenType struct {
ID uint `gorm:"primaryKey;autoIncrement"`
TokenHash string `gorm:"uniqueIndex:idx_token_hash_type,composite:token_hash_type;type:varchar(64);not null"`
TokenType string `gorm:"uniqueIndex:idx_token_hash_type,composite:token_hash_type;index:idx_token_type;type:varchar(20);not null"`
ExpiresAt time.Time `gorm:"index:idx_expires_at;not null"`
CreatedAt time.Time `gorm:"not null"`
}
RevokedTokenType represents a revoked token in the database. This model stores token hashes for revocation checking with automatic expiration.
Database Schema:
- id: Primary key with auto-increment
- token_hash: SHA-256 hash of the token (64 chars) with composite unique index
- token_type: Either "access" or "refresh" with index for efficient filtering
- expires_at: Expiration timestamp with index for efficient cleanup
- created_at: Creation timestamp for auditing
Security Considerations:
- Only token hashes are stored, never the actual tokens
- Composite unique index prevents duplicate revocations
- Automatic expiration via TTL-based cleanup
- Indexed fields optimize query performance
Storage Efficiency:
- Fixed-size hash storage (64 bytes vs variable token size)
- Automatic cleanup prevents unbounded growth
- Composite indexes reduce storage overhead
func (RevokedTokenType) TableName ยถ added in v1.0.6
func (RevokedTokenType) TableName() string
TableName specifies the table name for RevokedTokenType Override GORM's default pluralization for consistent naming
type RotatedTokenType ยถ added in v1.0.6
type RotatedTokenType struct {
ID uint `gorm:"primaryKey;autoIncrement"`
TokenHash string `gorm:"uniqueIndex:idx_rotated_token_hash;type:varchar(64);not null"`
ExpiresAt time.Time `gorm:"index:idx_rotated_expires_at;not null"`
CreatedAt time.Time `gorm:"not null"`
}
RotatedTokenType represents a rotated token in the database. This model stores hashes of rotated refresh tokens to prevent replay attacks.
Database Schema:
- id: Primary key with auto-increment
- token_hash: SHA-256 hash of the rotated token (64 chars) with unique index
- expires_at: Expiration timestamp with index for efficient cleanup
- created_at: Creation timestamp for auditing
Security Purpose:
- Prevents reuse of rotated refresh tokens
- Enables one-time use of refresh tokens during rotation
- Provides replay attack protection
Performance Optimizations:
- Unique index on token_hash enables fast lookups
- TTL index on expires_at enables efficient cleanup
- Fixed-size hash storage minimizes memory usage
func (RotatedTokenType) TableName ยถ added in v1.0.6
func (RotatedTokenType) TableName() string
TableName specifies the table name for RotatedTokenType Override GORM's default pluralization for consistent naming
type SigningMethod ยถ
type SigningMethod string
SigningMethod represents the cryptographic approach for signing tokens.
const ( // Symmetric uses HMAC-based algorithms (HS256, HS384, HS512) with a shared secret key. // Simpler to set up but requires secure key distribution. Symmetric SigningMethod = "symmetric" // Asymmetric uses public-key algorithms (RS256, ES256, PS256, EdDSA) with key pairs. // More secure for distributed systems where tokens are verified by multiple services. Asymmetric SigningMethod = "asymmetric" )
type TestRepositoryFactory ยถ added in v1.0.6
type TestRepositoryFactory func(t *testing.T) (TokenRepository, func())
type TokenRepository ยถ added in v1.0.6
type TokenRepository interface {
// MarkTokenRevoke marks a token as revoked with a time-to-live.
// The token should be stored until TTL expires, after which it can be cleaned up.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - tokenType: Type of token (AccessToken or RefreshToken)
// - token: The JWT token string to revoke
// - ttl: Time-to-live duration (should match token's remaining lifetime)
//
// Returns:
// - error: If revocation fails or context is cancelled
MarkTokenRevoke(ctx context.Context, tokenType TokenType, token string, ttl time.Duration) error
// IsTokenRevoked checks if a token has been revoked.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - tokenType: Type of token (AccessToken or RefreshToken)
// - token: The JWT token string to check
//
// Returns:
// - bool: true if token is revoked, false otherwise
// - error: If check fails or context is cancelled
IsTokenRevoked(ctx context.Context, tokenType TokenType, token string) (bool, error)
// MarkTokenRotated marks a refresh token as rotated (non-atomic).
// Use MarkTokenRotatedAtomic instead for production systems.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - token: The JWT token string to mark as rotated
// - ttl: Time-to-live duration
//
// Returns:
// - error: If marking fails or context is cancelled
MarkTokenRotated(ctx context.Context, token string, ttl time.Duration) error
// MarkTokenRotatedAtomic atomically marks a token as rotated using compare-and-swap.
// This prevents race conditions where multiple requests try to rotate the same token.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - token: The JWT token string to mark as rotated
// - ttl: Time-to-live duration
//
// Returns:
// - bool: true if successfully marked (first caller), false if already marked
// - error: If operation fails or context is cancelled
MarkTokenRotatedAtomic(ctx context.Context, token string, ttl time.Duration) (bool, error)
// IsTokenRotated checks if a refresh token has been rotated.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - token: The JWT token string to check
//
// Returns:
// - bool: true if token has been rotated, false otherwise
// - error: If check fails or context is cancelled
IsTokenRotated(ctx context.Context, token string) (bool, error)
// GetRotationTTL retrieves the remaining time-to-live for a rotated token entry.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - token: The JWT token string
//
// Returns:
// - time.Duration: Remaining TTL (0 if not found or expired)
// - error: If retrieval fails or context is cancelled
GetRotationTTL(ctx context.Context, token string) (time.Duration, error)
// CleanupExpiredRevokedTokens removes expired revoked tokens from storage.
// Should be called periodically by background cleanup goroutines.
//
// Parameters:
// - ctx: Context for cancellation and timeout
// - tokenType: Type of tokens to clean up
//
// Returns:
// - error: If cleanup fails or context is cancelled
CleanupExpiredRevokedTokens(ctx context.Context, tokenType TokenType) error
// CleanupExpiredRotatedTokens removes expired rotation markers from storage.
// Should be called periodically by background cleanup goroutines.
//
// Parameters:
// - ctx: Context for cancellation and timeout
//
// Returns:
// - error: If cleanup fails or context is cancelled
CleanupExpiredRotatedTokens(ctx context.Context) error
}
TokenRepository defines the interface for persistent token storage operations. Implementations must handle token revocation, rotation tracking, and cleanup of expired tokens.
Thread Safety:
All methods must be safe for concurrent use by multiple goroutines. MarkTokenRotatedAtomic must provide atomic compare-and-swap semantics.
Implementation Considerations:
- Use efficient storage (Redis recommended for production)
- Implement proper TTL handling to prevent memory leaks
- Consider using token hashes rather than storing full tokens
- Ensure MarkTokenRotatedAtomic is truly atomic to prevent race conditions
func NewGormTokenRepository ยถ added in v1.0.6
func NewGormTokenRepository(db *gorm.DB) (TokenRepository, error)
NewGormTokenRepository creates a new GORM-based token repository. Performs connection testing, schema validation, and automatic migrations.
Prerequisites:
- GORM database instance must be properly initialized
- Database user must have CREATE TABLE privileges
- Network connectivity to database server
- Sufficient connection pool size
Initialization Steps:
- Validate database connection is not nil
- Test database connectivity with 5-second timeout
- Auto-migrate revoked_tokens and rotated_tokens tables
- Create composite indexes for optimal query performance
- Return initialized repository instance
Parameters:
- db: Initialized GORM database instance. Must support transactions and migrations.
Returns:
- TokenRepository: Initialized GORM repository instance
- error: If connection fails, migration fails, or context times out
Example (PostgreSQL production):
// Initialize GORM first with connection pooling
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
PrepareStmt: true,
ConnPool: &sql.DB{
SetMaxOpenConns(100),
SetMaxIdleConns(10),
SetConnMaxLifetime(time.Hour),
},
})
if err != nil {
return err
}
// Create repository
repo, err := NewGormTokenRepository(db)
if err != nil {
return fmt.Errorf("failed to create token repository: %w", err)
}
Example (SQLite development):
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
repo, err := NewGormTokenRepository(db)
if err != nil {
log.Fatal(err)
}
Migration Details:
- Creates revoked_tokens and rotated_tokens tables if not exist
- Adds composite unique index on (token_hash, token_type)
- Adds individual indexes on token_type and expires_at
- Sets up foreign keys if supported by database
- Configures appropriate column types and constraints
func NewMemoryTokenRepository ยถ added in v1.0.6
func NewMemoryTokenRepository(cleanupInterval time.Duration) TokenRepository
NewMemoryTokenRepository creates a new in-memory token repository. Starts background cleanup goroutine for automatic expiration management.
Use Cases:
- Development and testing environments
- Single-instance applications without persistence requirements
- Prototyping and proof-of-concept implementations
- Low-security internal applications
- Applications where restart-based token reset is acceptable
Performance Optimizations:
- Separate maps for different token types reduce lock contention
- Read-preferring lock enables high concurrent read throughput
- Background cleanup avoids blocking main operations
- Hash-based storage enables O(1) operations
Parameters:
- cleanupInterval: How often to run background cleanup. Defaults to 5 minutes if <= 0. Shorter intervals reduce memory usage but increase CPU. Longer intervals increase memory usage but reduce cleanup overhead.
Returns:
- TokenRepository: Initialized in-memory repository with running cleanup goroutine
Example (Development with frequent cleanup):
// Clean up every minute for memory efficiency repo := NewMemoryTokenRepository(time.Minute)
Example (Testing with default cleanup):
// Use default 5-minute cleanup repo := NewMemoryTokenRepository(0)
Example (Production-like with hourly cleanup):
// Clean up hourly for reduced overhead repo := NewMemoryTokenRepository(time.Hour)
Goroutine Management:
- Cleanup goroutine started automatically
- Use Close() to stop goroutine during shutdown
- Automatic cleanup prevents memory leaks
func NewMongoTokenRepository ยถ added in v1.0.6
func NewMongoTokenRepository(db *mongo.Database, useTransactions bool) (TokenRepository, error)
NewMongoTokenRepository creates a new MongoDB-based token repository. Performs connection testing, index creation, and collection validation.
Prerequisites:
- MongoDB database instance must be properly initialized
- Database user must have index creation privileges
- Network connectivity to MongoDB server/cluster
- Sufficient connection pool size in MongoDB driver
Initialization Steps:
- Validate database connection is not nil
- Test MongoDB connectivity with 5-second timeout
- Create TTL and composite indexes for optimal performance
- Configure transaction usage based on parameter
- Return initialized repository instance
Parameters:
- db: Initialized MongoDB database instance from official driver
- useTransactions: Whether to use MongoDB transactions for multi-document operations. Recommended for production with replica sets. Disable for standalone instances.
Returns:
- TokenRepository: Initialized MongoDB repository instance
- error: If connection fails, index creation fails, or context times out
Example (Production with transactions):
// Initialize MongoDB client with connection pooling
client, err := mongo.Connect(ctx, options.Client().
ApplyURI(mongoURI).
SetMaxPoolSize(100).
SetMinPoolSize(10))
if err != nil {
return err
}
db := client.Database("auth_service")
repo, err := NewMongoTokenRepository(db, true) // Enable transactions
if err != nil {
return fmt.Errorf("failed to create MongoDB token repository: %w", err)
}
Example (Development without transactions):
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
db := client.Database("dev_auth")
repo, err := NewMongoTokenRepository(db, false) // Disable transactions for standalone
if err != nil {
log.Fatal(err)
}
Index Creation Details:
- Creates composite unique index on (token_hash, token_type) for revoked tokens
- Creates TTL index on expires_at for automatic document expiration
- Creates individual index on token_type for efficient filtering
- Creates unique index on token_hash for rotated tokens
func NewRedisTokenRepository ยถ added in v1.0.6
func NewRedisTokenRepository(client *redis.Client) (TokenRepository, error)
NewRedisTokenRepository creates a new Redis-based token repository. Performs connection testing and client validation.
Prerequisites:
- Redis client must be properly initialized and configured
- Network connectivity to Redis server/cluster
- Sufficient memory allocation in Redis
- Proper maxmemory policy configuration
Initialization Steps:
- Validate Redis client is not nil
- Test Redis connectivity with 5-second timeout
- Return initialized repository instance
Parameters:
- client: Initialized Redis client from go-redis library
Returns:
- TokenRepository: Initialized Redis repository instance
- error: If connection fails or client is nil
Example (Redis standalone):
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password
DB: 0, // default DB
PoolSize: 100,
})
repo, err := NewRedisTokenRepository(client)
if err != nil {
return fmt.Errorf("failed to create Redis token repository: %w", err)
}
Example (Redis Cluster):
client := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"redis-node1:6379", "redis-node2:6379", "redis-node3:6379"},
Password: "your-redis-password",
PoolSize: 100,
})
repo, err := NewRedisTokenRepository(client)
if err != nil {
return fmt.Errorf("failed to create Redis token repository: %w", err)
}
Production Considerations:
- Configure connection pooling appropriately
- Set up Redis persistence if data durability required
- Implement Redis monitoring and alerting
- Consider Redis Sentinel for high availability
- Use Redis Cluster for horizontal scaling
type TokenType ยถ
type TokenType string
TokenType represents the type of JWT token (access or refresh). Access tokens are short-lived and used for API authorization. Refresh tokens are longer-lived and used to obtain new access tokens.
const ( // AccessToken represents a short-lived token used for API authorization. // Typically includes user roles and permissions. AccessToken TokenType = "access" // RefreshToken represents a long-lived token used to obtain new access tokens. // Should be stored securely and rotated regularly. RefreshToken TokenType = "refresh" )