Add comprehensive JWT module documentation

2026-01-11 16:11:08 +11:00
parent 469914d915
commit f1799917d8
2 changed files with 555 additions and 1 deletions

19
Home.md

@@ -1 +1,18 @@
Welcome to the Wiki.
# 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).

537
JWT.md Normal file

@@ -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.