From f1799917d85618293a4ea50008ef0aa80dfc82b2 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Sun, 11 Jan 2026 16:11:08 +1100 Subject: [PATCH] Add comprehensive JWT module documentation --- Home.md | 19 +- JWT.md | 537 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 JWT.md diff --git a/Home.md b/Home.md index 5d08b7b..80cdb30 100644 --- a/Home.md +++ b/Home.md @@ -1 +1,18 @@ -Welcome to the Wiki. \ No newline at end of file +# golib Wiki + +Welcome to the golib documentation wiki. This wiki provides comprehensive documentation for all golib modules. + +## Modules + +### [JWT](JWT.md) +JWT (JSON Web Token) generation and validation with database-backed token revocation support. Supports multiple database backends and ORMs. + +## Installation + +```bash +go get git.haelnorr.com/h/golib/jwt +``` + +## Contributing + +For issues, feature requests, or contributions, please visit the [golib repository](https://git.haelnorr.com/h/golib). \ No newline at end of file diff --git a/JWT.md b/JWT.md new file mode 100644 index 0000000..df4a7df --- /dev/null +++ b/JWT.md @@ -0,0 +1,537 @@ +# JWT Package + +The `jwt` package provides comprehensive JWT (JSON Web Token) generation and validation with database-backed token revocation support. + +## Features + +- **Access and Refresh Tokens**: Separate tokens for authentication and token renewal +- **Token Revocation**: Database-backed blacklist for revoked tokens +- **Multi-Database Support**: Works with PostgreSQL, MySQL, SQLite, and MariaDB +- **ORM Compatibility**: Compatible with `database/sql`, GORM, and Bun +- **Automatic Table Management**: Creates and manages blacklist table automatically +- **Native Cleanup**: Database-specific automatic cleanup (PostgreSQL functions, MySQL events) +- **Token Freshness**: Track token freshness for sensitive operations +- **Remember Me**: Support for session vs persistent tokens + +## Installation + +```bash +go get git.haelnorr.com/h/golib/jwt +``` + +## Quick Start + +### Basic Setup (with Database) + +```go +package main + +import ( + "database/sql" + "git.haelnorr.com/h/golib/jwt" + _ "github.com/lib/pq" +) + +func main() { + // Open database connection + db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable") + if err != nil { + panic(err) + } + defer db.Close() + + // Create token generator + gen, err := jwt.CreateGenerator(jwt.GeneratorConfig{ + AccessExpireAfter: 15, // Access tokens expire in 15 minutes + RefreshExpireAfter: 1440, // Refresh tokens expire in 24 hours (1440 minutes) + FreshExpireAfter: 5, // Tokens stay fresh for 5 minutes + TrustedHost: "example.com", + SecretKey: "your-secret-key-here", + DBConn: db, + DBType: jwt.DatabaseType{ + Type: jwt.DatabasePostgreSQL, + Version: "15", + }, + TableConfig: jwt.DefaultTableConfig(), + }) + if err != nil { + panic(err) + } + + // Generate tokens + accessToken, accessExp, err := gen.NewAccess(userID, true, false) + refreshToken, refreshExp, err := gen.NewRefresh(userID, false) +} +``` + +### Without Database (No Revocation) + +```go +gen, err := jwt.CreateGenerator(jwt.GeneratorConfig{ + AccessExpireAfter: 15, + RefreshExpireAfter: 1440, + FreshExpireAfter: 5, + TrustedHost: "example.com", + SecretKey: "your-secret-key-here", + DBConn: nil, // No database = no revocation +}) +``` + +## Configuration + +### GeneratorConfig + +```go +type GeneratorConfig struct { + // Required fields + AccessExpireAfter int64 // Access token expiry in minutes + RefreshExpireAfter int64 // Refresh token expiry in minutes + FreshExpireAfter int64 // Token freshness duration in minutes + TrustedHost string // Trusted hostname for token issuer + SecretKey string // Secret key for token signing + + // Optional fields (for database support) + DBConn *sql.DB // Database connection (nil to disable revocation) + DBType DatabaseType // Database type and version + TableConfig TableConfig // Table configuration +} +``` + +### DatabaseType + +Supported database types with their constants: + +```go +jwt.DatabasePostgreSQL // "postgres" +jwt.DatabaseMySQL // "mysql" +jwt.DatabaseSQLite // "sqlite" +jwt.DatabaseMariaDB // "mariadb" +``` + +Example: + +```go +DBType: jwt.DatabaseType{ + Type: jwt.DatabasePostgreSQL, + Version: "15.3", +} +``` + +### TableConfig + +```go +type TableConfig struct { + TableName string // Blacklist table name (default: "jwtblacklist") + AutoCreate bool // Auto-create table if missing (default: true) + EnableAutoCleanup bool // Enable database-native cleanup (default: true) + CleanupInterval int // Cleanup interval in hours (default: 24) +} +``` + +Get default configuration: + +```go +config := jwt.DefaultTableConfig() +config.TableName = "my_custom_blacklist" // Customize as needed +``` + +## Token Generation + +### Access Tokens + +```go +// NewAccess(subject int, fresh bool, rememberMe bool) +token, expiry, err := gen.NewAccess(42, true, false) +``` + +Parameters: +- `subject`: User ID or subject identifier +- `fresh`: Whether token should be marked as fresh (for sensitive operations) +- `rememberMe`: If true, token is persistent; if false, session-only + +### Refresh Tokens + +```go +// NewRefresh(subject int, rememberMe bool) +token, expiry, err := gen.NewRefresh(42, true) +``` + +## Token Validation + +Tokens must be validated within a transaction context: + +```go +// Begin transaction +tx, err := db.Begin() +if err != nil { + return err +} +defer tx.Rollback() + +// Create executor +exec := jwt.NewSQLExecutor(tx) + +// Validate access token +token, err := gen.ValidateAccess(exec, tokenString) +if err != nil { + return err // Token invalid or revoked +} + +// Validate refresh token +token, err := gen.ValidateRefresh(exec, tokenString) +if err != nil { + return err // Token invalid or revoked +} + +// Commit transaction +tx.Commit() +``` + +### Token Properties + +After validation, you can access token claims: + +```go +token, _ := gen.ValidateAccess(exec, tokenString) + +userID := token.SUB // Subject (user ID) +issuer := token.ISS // Issuer (trusted host) +expiry := token.EXP // Expiration timestamp +issuedAt := token.IAT // Issued at timestamp +freshUntil := token.Fresh // Fresh until timestamp +tokenID := token.JTI // Unique token identifier +scope := token.Scope // "access" or "refresh" +ttl := token.TTL // "session" or "exp" +``` + +## Token Revocation + +### Revoking a Token + +```go +tx, _ := db.Begin() +defer tx.Rollback() + +exec := jwt.NewSQLExecutor(tx) + +// Validate token first +token, err := gen.ValidateAccess(exec, tokenString) +if err != nil { + return err +} + +// Revoke the token +err = token.Revoke(exec) +if err != nil { + return err +} + +tx.Commit() +``` + +### Checking Revocation Status + +```go +isValid, err := token.CheckNotRevoked(exec) +if !isValid { + // Token has been revoked +} +``` + +## Token Freshness + +Tokens can be marked as "fresh" for sensitive operations: + +```go +// Generate fresh token +accessToken, _, _ := gen.NewAccess(userID, true, false) // fresh=true + +// Check freshness +token, _ := gen.ValidateAccess(exec, accessToken) +currentTime := time.Now().Unix() +if currentTime > token.Fresh { + // Token is stale, require re-authentication + return errors.New("token is not fresh") +} +``` + +## Cleanup + +### Manual Cleanup + +Remove expired tokens from the blacklist: + +```go +import "context" + +err := gen.Cleanup(context.Background()) +``` + +### Automatic Cleanup + +#### PostgreSQL + +The package creates a cleanup function that you can schedule: + +```sql +-- Call manually +SELECT cleanup_jwtblacklist(); + +-- Or schedule with pg_cron extension +SELECT cron.schedule('cleanup-jwt', '0 * * * *', 'SELECT cleanup_jwtblacklist()'); +``` + +#### MySQL/MariaDB + +An automatic event is created (requires `event_scheduler` to be enabled): + +```sql +-- Check if event scheduler is enabled +SHOW VARIABLES LIKE 'event_scheduler'; + +-- Enable if needed +SET GLOBAL event_scheduler = ON; + +-- View the created event +SHOW EVENTS LIKE 'cleanup_jwtblacklist_event'; +``` + +#### SQLite + +SQLite doesn't support automatic cleanup. Use manual cleanup: + +```go +// Run periodically (e.g., via cron or background goroutine) +ticker := time.NewTicker(24 * time.Hour) +go func() { + for range ticker.C { + gen.Cleanup(context.Background()) + } +}() +``` + +## Database Schema + +The blacklist table is created automatically with the following structure: + +### PostgreSQL + +```sql +CREATE TABLE IF NOT EXISTS jwtblacklist ( + jti UUID PRIMARY KEY, + exp BIGINT NOT NULL, + sub INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_jwtblacklist_exp ON jwtblacklist(exp); +CREATE INDEX IF NOT EXISTS idx_jwtblacklist_sub ON jwtblacklist(sub); +``` + +### MySQL/MariaDB + +```sql +CREATE TABLE IF NOT EXISTS jwtblacklist ( + jti CHAR(36) PRIMARY KEY, + exp BIGINT NOT NULL, + sub INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_exp (exp), + INDEX idx_sub (sub) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### SQLite + +```sql +CREATE TABLE IF NOT EXISTS jwtblacklist ( + jti TEXT PRIMARY KEY, + exp INTEGER NOT NULL, + sub INTEGER NOT NULL, + created_at INTEGER DEFAULT (strftime('%s', 'now')) +); +CREATE INDEX IF NOT EXISTS idx_jwtblacklist_exp ON jwtblacklist(exp); +CREATE INDEX IF NOT EXISTS idx_jwtblacklist_sub ON jwtblacklist(sub); +``` + +## Using with ORMs + +### GORM + +```go +import "gorm.io/gorm" + +// Get underlying *sql.DB from GORM +gormDB, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{}) +sqlDB, _ := gormDB.DB() + +// Create generator with sql.DB +gen, _ := jwt.CreateGenerator(jwt.GeneratorConfig{ + // ... config + DBConn: sqlDB, +}) + +// Use with transactions +tx := gormDB.Begin() +sqlTx, _ := tx.DB() +exec := jwt.NewSQLExecutor(sqlTx) +token, _ := gen.ValidateAccess(exec, tokenString) +tx.Commit() +``` + +### Bun + +```go +import "github.com/uptrace/bun" + +// Get underlying *sql.DB from Bun +bunDB := bun.NewDB(sqldb, pgdialect.New()) +sqlDB := bunDB.DB // Already *sql.DB + +// Create generator +gen, _ := jwt.CreateGenerator(jwt.GeneratorConfig{ + // ... config + DBConn: sqlDB, +}) + +// Use with transactions +tx, _ := bunDB.Begin() +sqlTx, _ := tx.DB() +exec := jwt.NewSQLExecutor(sqlTx) +token, _ := gen.ValidateAccess(exec, tokenString) +tx.Commit() +``` + +## Complete Example + +```go +package main + +import ( + "context" + "database/sql" + "fmt" + "time" + + "git.haelnorr.com/h/golib/jwt" + _ "github.com/lib/pq" +) + +func main() { + // Setup database + db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb?sslmode=disable") + if err != nil { + panic(err) + } + defer db.Close() + + // Create token generator + gen, err := jwt.CreateGenerator(jwt.GeneratorConfig{ + AccessExpireAfter: 15, + RefreshExpireAfter: 1440, + FreshExpireAfter: 5, + TrustedHost: "example.com", + SecretKey: "super-secret-key", + DBConn: db, + DBType: jwt.DatabaseType{ + Type: jwt.DatabasePostgreSQL, + Version: "15", + }, + TableConfig: jwt.DefaultTableConfig(), + }) + if err != nil { + panic(err) + } + + // Generate tokens for user 42 + userID := 42 + accessToken, accessExp, err := gen.NewAccess(userID, true, false) + if err != nil { + panic(err) + } + refreshToken, refreshExp, err := gen.NewRefresh(userID, false) + if err != nil { + panic(err) + } + + fmt.Printf("Access Token: %s (expires: %d)\n", accessToken, accessExp) + fmt.Printf("Refresh Token: %s (expires: %d)\n", refreshToken, refreshExp) + + // Validate access token + tx, _ := db.Begin() + exec := jwt.NewSQLExecutor(tx) + + token, err := gen.ValidateAccess(exec, accessToken) + if err != nil { + panic(err) + } + + fmt.Printf("Token valid for user: %d\n", token.SUB) + + // Revoke token + err = token.Revoke(exec) + if err != nil { + panic(err) + } + + tx.Commit() + + // Try to validate revoked token + tx, _ = db.Begin() + exec = jwt.NewSQLExecutor(tx) + + _, err = gen.ValidateAccess(exec, accessToken) + if err != nil { + fmt.Println("Token is revoked:", err) + } + + tx.Commit() + + // Cleanup expired tokens + err = gen.Cleanup(context.Background()) + if err != nil { + panic(err) + } +} +``` + +## Best Practices + +1. **Use Strong Secret Keys**: Generate cryptographically secure secret keys +2. **Keep Expiry Times Short**: Short-lived access tokens reduce security risk +3. **Enable Database Revocation**: Always use database revocation for production +4. **Regular Cleanup**: Schedule regular cleanup of expired tokens +5. **Check Token Freshness**: Require fresh tokens for sensitive operations +6. **Use HTTPS**: Always transmit tokens over secure connections +7. **Transaction Management**: Always use transactions for revocation operations +8. **Monitor Blacklist Size**: Keep an eye on blacklist table growth + +## Troubleshooting + +### Table Not Created + +If the table isn't auto-created, check: +- Database connection is valid +- User has CREATE TABLE permissions +- `AutoCreate` is set to `true` in TableConfig + +### Cleanup Not Working + +**PostgreSQL:** +- Ensure the cleanup function exists: `SELECT routine_name FROM information_schema.routines WHERE routine_name LIKE 'cleanup_%';` +- Schedule with pg_cron or call manually + +**MySQL:** +- Check event scheduler: `SHOW VARIABLES LIKE 'event_scheduler';` +- Enable if needed: `SET GLOBAL event_scheduler = ON;` +- Check events: `SHOW EVENTS;` + +### Performance Issues + +If blacklist queries are slow: +- Ensure indexes exist on `jti`, `exp`, and `sub` columns +- Run cleanup more frequently +- Consider partitioning the table by date + +## API Reference + +See the [GoDoc documentation](https://pkg.go.dev/git.haelnorr.com/h/golib/jwt) for complete API reference.