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