Add comprehensive JWT module documentation
19
Home.md
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
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.
|
||||
Reference in New Issue
Block a user