Documentation
¶
Overview ¶
Package jwt implements JWT (JSON Web Token) authentication for Aegis.
This plugin provides stateless token-based authentication using industry-standard JWT tokens with RSA signing. Features include:
- Token Generation: Access tokens (15min) and refresh tokens (7d)
- RSA Signing: RS256 algorithm with database-backed key storage
- Key Rotation: Automatic key rotation with configurable intervals
- JWKS Endpoint: Public key discovery for token verification
- Token Blacklist: Redis-backed token revocation
- Refresh Flow: Secure token refresh without re-authentication
JWT vs Session Tokens:
Session Tokens (core): - Stateful: Requires database lookup on every request - Revocable: Can be deleted from database immediately - Simple: Just random strings, no cryptography JWT Tokens (this plugin): - Stateless: Can be verified without database (using public key) - Self-contained: Includes user ID and expiry in token - Distributed: Multiple services can verify without shared database - Revocation: Requires blacklist (Redis) for immediate revocation
Architecture:
Token Pair: - Access Token: Short-lived (15min), used for API requests - Refresh Token: Long-lived (7d), used to get new access tokens Key Storage: - Private keys: Stored in database, used for signing - Public keys: Exposed via /jwt/.well-known/jwks.json (JWKS endpoint) - Key rotation: Old keys retained for token verification Token Flow: 1. User authenticates (email/password, OAuth, etc.) 2. POST /jwt/token → Get access + refresh token 3. Use access token in Authorization header 4. When access token expires, POST /jwt/refreshToken with refresh token 5. Get new access + refresh token pair
Use Cases:
- Microservices: Stateless authentication across services
- Mobile apps: Long-lived refresh tokens
- SPAs: JavaScript apps with token storage
- Third-party integrations: Standard JWT format
Security Considerations:
- Access tokens are short-lived (minimize exposure window)
- Refresh tokens are long-lived (balance UX vs security)
- Token blacklist requires Redis (for logout/revocation)
- HTTPS is REQUIRED (tokens are bearer credentials)
- Store tokens securely (httpOnly cookies, secure storage, not localStorage)
Example:
package main
import (
"context"
"github.com/theinventorylib/aegis"
"github.com/theinventorylib/aegis/plugins/jwt"
)
func main() {
a, _ := aegis.New(context.Background(), ...)
// Configure JWT plugin
jwtConfig := &jwt.Config{
Issuer: "myapp",
AccessTokenExpiry: 15 * time.Minute,
RefreshTokenExpiry: 7 * 24 * time.Hour,
KeyRotationInterval: 24 * time.Hour,
}
// Register JWT plugin
a.Use(context.Background(), jwt.New(jwtConfig, nil, plugins.DialectPostgres))
a.MountRoutes("/auth")
// Routes available:
// POST /auth/jwt/token
// POST /auth/jwt/getAccessToken
// POST /auth/jwt/refreshToken
// POST /auth/jwt/logout
// GET /auth/jwt/.well-known/jwks.json
}
Package jwt provides database migration management for the JWT plugin.
This file handles parsing and loading migration files from the embedded filesystem, supporting multiple SQL dialects (PostgreSQL, MySQL, SQLite).
Migration Versioning:
- Version 001: Initial schema (internal/sql/<dialect>/schema.sql)
- Version 002+: Additional migrations (migrations/<dialect>/<version>_<desc>.<up|down>.sql)
File Naming Convention:
- Up migrations: 002_altered.up.sql, 003_add_index.up.sql
- Down migrations: 002_altered.down.sql, 003_add_index.down.sql
Directory Structure:
jwt/
internal/sql/
postgres/schema.sql
mysql/schema.sql
migrations/
postgres/
002_altered.up.sql
002_altered.down.sql
mysql/
002_altered.up.sql
002_altered.down.sql
The migration system ensures the correct schema version is applied for each SQL dialect, supporting forward (up) and backward (down) migrations.
Index ¶
- Constants
- func GetMigrations(dialect plugins.Dialect) ([]plugins.Migration, error)
- func GetSchema(dialect plugins.Dialect) (*plugins.Schema, error)
- func GetSchemaRequirements(dialect plugins.Dialect) []plugins.SchemaRequirement
- type Claims
- type Config
- type DefaultJWTStore
- func (s *DefaultJWTStore) DeleteExpiredJWKS(ctx context.Context) error
- func (s *DefaultJWTStore) GetCurrentJWK(ctx context.Context, algorithm, use string) (jwk.Key, error)
- func (s *DefaultJWTStore) ListJWKS(ctx context.Context) ([]JWK, error)
- func (s *DefaultJWTStore) StoreJWK(ctx context.Context, key jwk.Key, algorithm, use string, expiresAt *time.Time) error
- type Handler
- type JWK
- type Plugin
- func (p *Plugin) BlacklistToken(tokenStr string) error
- func (p *Plugin) CleanupExpiredKeys(ctx context.Context) error
- func (p *Plugin) Dependencies() []plugins.Dependency
- func (p *Plugin) Description() string
- func (p *Plugin) GenerateTokenPair(userID string) (*TokenPair, error)
- func (p *Plugin) GetMigrations() []plugins.Migration
- func (p *Plugin) GetSchemas() []plugins.Schema
- func (p *Plugin) Init(ctx context.Context, aegis plugins.Aegis) error
- func (p *Plugin) Logout(tokenStr string) error
- func (p *Plugin) LogoutAllSessions(userID string) error
- func (p *Plugin) MountRoutes(router router.Router, basePath string)
- func (p *Plugin) Name() string
- func (p *Plugin) ProvidesAuthMethods() []string
- func (p *Plugin) RefreshTokens(refreshToken string) (*TokenPair, error)
- func (p *Plugin) RequiresTables() []string
- func (p *Plugin) RotateKeys(ctx context.Context) error
- func (p *Plugin) StartKeyRotation(ctx context.Context)
- func (p *Plugin) ValidateToken(tokenStr string) (*Claims, error)
- func (p *Plugin) Version() string
- type Provider
- type RefreshTokenRequest
- type Store
- type TokenPair
- type TokenRequest
Constants ¶
const ( // TokenTypeAccess identifies an access token in JWT claims. // Access tokens are short-lived and used for API requests. TokenTypeAccess = "access" // TokenTypeRefresh identifies a refresh token in JWT claims. // Refresh tokens are long-lived and used to obtain new access tokens. TokenTypeRefresh = "refresh" )
const ( // SchemaTokenRequest is the OpenAPI schema name for POST /getToken request body. // Currently unused as /getToken accepts no body (uses authenticated user from context). SchemaTokenRequest = "TokenRequest" // SchemaRefreshTokenRequest is the OpenAPI schema name for POST /refreshToken request. // Request Body: // { // "refresh_token": "eyJhbGc..." // } SchemaRefreshTokenRequest = "RefreshTokenRequest" // SchemaTokenPair is the OpenAPI schema name for token pair responses. // Response Body: // { // "access_token": "eyJhbGc...", // "access_expiry": "2024-01-01T12:15:00Z", // "refresh_token": "eyJhbGc...", // "refresh_expiry": "2024-01-08T12:00:00Z" // } // // Used by: // - POST /getToken (success response) // - POST /getAccessToken (partial - access token only) // - POST /refreshToken (success response) SchemaTokenPair = "TokenPair" )
Schema names for OpenAPI specification generation.
These constants define the OpenAPI schema names for JWT request and response types. They are used in route metadata to generate accurate API documentation with typed request/response examples.
OpenAPI Integration: When routes are registered with schema metadata, the OpenAPI plugin uses these constants to link HTTP endpoints to their typed request/response structures.
Usage in Route Metadata:
route := core.Route{
Path: "/getToken",
Handler: handler.HandleGetToken,
Metadata: map[string]any{
"openapi": map[string]any{
"summary": "Generate JWT tokens",
"responses": map[string]any{
"200": map[string]any{
"description": "Token pair generated",
"schema": jwt.SchemaTokenPair,
},
},
},
},
}
Generated OpenAPI: The plugin converts these schema references into OpenAPI 3.0 schema definitions with JSON examples, field types, and validation rules.
Variables ¶
This section is empty.
Functions ¶
func GetMigrations ¶
GetMigrations returns all database migrations for the specified SQL dialect.
This function combines the initial schema (version 001) with any additional migrations (version 002+) to produce a complete, ordered list of migrations.
Version Numbering:
- Version 001: Always the initial schema.sql (CREATE TABLE statements)
- Version 002+: Additional migrations from migrations/<dialect>/ directory
File Naming:
- Format: <version>_<description>.<type>.sql
- Example: 002_add_expiry_index.up.sql
- Type: "up" (apply) or "down" (rollback)
Migration Loading:
- Load schema.sql as version 001 with no down migration
- Scan migrations/<dialect>/ for version 002+ files
- Parse version number and type (up/down) from filename
- Group up/down migrations by version
- Sort by version number ascending
- Return ordered migration list
Parameters:
- dialect: SQL dialect (DialectPostgres, DialectMySQL, DialectSQLite)
Returns:
- []plugins.Migration: Ordered list of migrations (version ASC)
- error: File read error, invalid filename, or unsupported dialect
Example:
migrations, err := GetMigrations(plugins.DialectPostgres)
if err != nil {
log.Fatal(err)
}
for _, m := range migrations {
fmt.Printf("Version %d: %s\n", m.Version, m.Description)
// Execute m.Up SQL to apply migration
}
func GetSchema ¶
GetSchema returns the initial database schema for the JWT plugin.
This function provides the CREATE TABLE statement for the jwks table, formatted for the specified SQL dialect. The schema is version 001 (initial setup) and should be applied before any incremental migrations.
Schema Purpose: The jwks table stores JSON Web Keys (JWKs) for JWT signing and verification. It supports key rotation by storing multiple keys with expiration timestamps.
Supported Dialects:
- PostgreSQL: Uses TEXT and TIMESTAMP types
- MySQL: Uses VARCHAR and DATETIME types
- SQLite: Not currently supported (can use PostgreSQL schema)
Parameters:
- dialect: SQL dialect (DialectPostgres or DialectMySQL)
Returns:
- *plugins.Schema: Schema metadata with SQL and version info
- error: Unsupported dialect error
Example:
schema, err := GetSchema(plugins.DialectPostgres)
if err != nil {
log.Fatal(err)
}
fmt.Println("Schema version:", schema.Info.Version)
// Execute schema.SQL to create table
func GetSchemaRequirements ¶
func GetSchemaRequirements(dialect plugins.Dialect) []plugins.SchemaRequirement
GetSchemaRequirements returns schema validation requirements for the JWT plugin.
This function defines what database objects must exist for the plugin to function. The framework uses these requirements to validate the schema before plugin initialization.
Requirements:
- Table "jwks" must exist: Stores JSON Web Keys for JWT signing/verification
Validation Timing:
- Called during plugin.Init() after migrations are applied
- Plugin initialization fails if requirements are not met
- Prevents runtime errors from missing database objects
Parameters:
- dialect: SQL dialect (DialectPostgres, DialectMySQL, etc.)
Returns:
- []plugins.SchemaRequirement: List of validation checks to perform
- Empty slice if dialect is not supported
Example:
reqs := GetSchemaRequirements(plugins.DialectPostgres)
for _, req := range reqs {
if err := req.Validate(db); err != nil {
log.Fatal("Schema validation failed:", err)
}
}
Types ¶
type Claims ¶
type Claims struct {
// UserID is the authenticated user's ID
// Used for quick user lookup without database query
UserID string `json:"user_id"`
// TokenType indicates if this is an "access" or "refresh" token
// Access tokens can be used for API requests
// Refresh tokens can only be used to get new access tokens
TokenType string `json:"token_type"`
}
Claims defines the structure of JWT token claims.
JWT claims are the payload embedded in the token. These are NOT encrypted, only signed - anyone can decode and read them. Don't include sensitive data.
Standard claims (JWT spec):
- iss: Issuer (from Config.Issuer)
- sub: Subject (UserID)
- exp: Expiration time
- iat: Issued at time
- jti: JWT ID (for revocation tracking)
Custom claims (Aegis):
- user_id: User ID for quick lookup
- token_type: "access" or "refresh"
Example token payload:
{
"iss": "aegis",
"sub": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"exp": 1704114900,
"iat": 1704114000,
"user_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"token_type": "access"
}
type Config ¶
type Config struct {
// Issuer is the JWT "iss" claim identifying the token issuer.
// Should be your application name or domain (e.g., "myapp.com").
Issuer string
// AccessTokenExpiry is how long access tokens remain valid.
// Recommended: 15 minutes to 1 hour
// Shorter = more secure, longer = fewer refresh requests
AccessTokenExpiry time.Duration
// RefreshTokenExpiry is how long refresh tokens remain valid.
// Recommended: 7 days to 30 days
// Shorter = more secure, longer = better UX (less frequent logins)
RefreshTokenExpiry time.Duration
// KeyRotationInterval is how often to rotate signing keys.
// Recommended: 24 hours to 7 days
// More frequent rotation limits the impact of key compromise.
// Set to 0 to disable automatic rotation (manual rotation only).
KeyRotationInterval time.Duration
// KeySize is the RSA key size in bits (2048, 3072, or 4096).
// Only used for RSA algorithm.
// Recommended: 2048 (good balance of security and performance)
// 4096 provides higher security but slower signing/verification.
KeySize int
// KeyAlgorithm indicates the signing algorithm.
// Currently supported: "RSA" (uses RS256)
// Future: ECDSA (ES256), HMAC (HS256) for symmetric keys
KeyAlgorithm string
// KeyRetention is how long to keep rotated keys in storage.
// MUST be greater than the maximum token lifetime (RefreshTokenExpiry)
// to ensure old tokens can still be verified.
// Recommended: 30 days (covers refresh token lifetime + buffer)
KeyRetention time.Duration
}
Config holds JWT plugin configuration.
All durations should be balanced for security vs user experience:
- Shorter access tokens: More secure (tokens expire quickly)
- Longer refresh tokens: Better UX (less frequent re-authentication)
- Frequent key rotation: More secure (limits key exposure)
- Longer key retention: Required for old token validation
func DefaultConfig ¶
func DefaultConfig() *Config
DefaultConfig returns production-ready JWT configuration with security best practices.
Default values:
- Issuer: "aegis"
- Access token: 15 minutes (short-lived for security)
- Refresh token: 7 days (balance between UX and security)
- Key rotation: 24 hours (daily rotation limits key exposure)
- Key size: 2048 bits (industry standard RSA key size)
- Algorithm: RSA/RS256 (asymmetric signing)
- Key retention: 30 days (covers refresh token lifetime)
Customize for your use case:
config := jwt.DefaultConfig() config.Issuer = "myapp.com" config.AccessTokenExpiry = 1 * time.Hour // Longer for internal APIs config.RefreshTokenExpiry = 30 * 24 * time.Hour // 30 days for mobile apps
type DefaultJWTStore ¶
type DefaultJWTStore struct {
// contains filtered or unexported fields
}
DefaultJWTStore implements JWTStore using a SQL database backend.
This implementation uses sqlc-generated type-safe queries to manage JWK storage in PostgreSQL, MySQL, or SQLite. It stores JWKs as JSON-serialized data in the jwks table, indexed by key ID (kid), algorithm, and use type.
Database Schema:
- kid: Unique key identifier (matches JWT "kid" claim)
- key_data: JSON-serialized JWK (includes public and private key material)
- algorithm: Signing algorithm (e.g., "RS256", "ES256")
- use: Key usage ("sig" for signatures, "enc" for encryption)
- created_at: Timestamp when key was stored
- expires_at: Optional expiration timestamp for key rotation
Thread Safety: This store is safe for concurrent use through database transactions. Multiple processes can share the same database without coordination.
Example Usage:
db, _ := sql.Open("postgres", "postgresql://localhost/aegis")
store := NewDefaultJWTStore(db)
// Retrieve current signing key
key, err := store.GetCurrentJWK(ctx, "RS256", "sig")
// Store new key with expiration
expiry := time.Now().Add(30 * 24 * time.Hour)
err = store.StoreJWK(ctx, newKey, "RS256", "sig", &expiry)
// Clean up expired keys
err = store.DeleteExpiredJWKS(ctx)
func NewDefaultJWTStore ¶
func NewDefaultJWTStore(db *sql.DB) *DefaultJWTStore
NewDefaultJWTStore creates a new DefaultJWTStore backed by a SQL database.
The provided database connection must be configured for the correct dialect (PostgreSQL, MySQL, or SQLite) and have the jwks table schema applied.
Parameters:
- db: Active SQL database connection (must have jwks table)
Returns:
- Configured DefaultJWTStore ready for use
Example:
db, _ := sql.Open("postgres", "postgresql://localhost/aegis")
store := NewDefaultJWTStore(db)
func (*DefaultJWTStore) DeleteExpiredJWKS ¶
func (s *DefaultJWTStore) DeleteExpiredJWKS(ctx context.Context) error
DeleteExpiredJWKS removes all expired keys from the database.
This method should be called periodically (e.g., via background job) to clean up old keys after their retention period has passed. Keys are only deleted if:
- expires_at IS NOT NULL AND expires_at < NOW()
Keys without expiration (expires_at = NULL) are never deleted automatically. This supports long-lived keys that don't rotate.
Cleanup Strategy:
- Run on plugin initialization to remove stale keys from previous runs
- Schedule periodic cleanup (e.g., daily cron job)
- Safe to call frequently (only deletes expired keys)
Returns:
- error: Database error if deletion fails
Example:
// Clean up expired keys daily
go func() {
ticker := time.NewTicker(24 * time.Hour)
for range ticker.C {
store.DeleteExpiredJWKS(context.Background())
}
}()
func (*DefaultJWTStore) GetCurrentJWK ¶
func (s *DefaultJWTStore) GetCurrentJWK(ctx context.Context, algorithm, use string) (jwk.Key, error)
GetCurrentJWK retrieves the most recent non-expired JWK from the database matching the specified algorithm and use type.
This method queries for the newest key (by created_at DESC) that:
- Matches the specified algorithm (e.g., "RS256")
- Matches the use type ("sig" for signing, "enc" for encryption)
- Is not expired (expires_at > NOW() OR expires_at IS NULL)
The returned key includes both public and private key material for signing. For public-only keys (verification), use ListJWKS and extract PublicKey().
Parameters:
- ctx: Request context (supports cancellation)
- algorithm: Key algorithm (e.g., "RS256", "ES256")
- use: Key usage ("sig" for signatures, "enc" for encryption)
Returns:
- jwk.Key: Parsed JWK with public and private key material
- error: Database error, parsing error, or ErrNoRows if no key found
Example:
// Get current RSA signing key
key, err := store.GetCurrentJWK(ctx, "RS256", "sig")
if err != nil {
log.Fatal("No signing key available")
}
func (*DefaultJWTStore) ListJWKS ¶
func (s *DefaultJWTStore) ListJWKS(ctx context.Context) ([]JWK, error)
ListJWKS retrieves all non-expired JWKs from the database.
This method returns keys that are currently valid for token verification:
- expires_at IS NULL (never expires), OR
- expires_at > NOW() (not yet expired)
Used by the JWKS endpoint (/.well-known/jwks.json) to publish public keys for JWT verification by external services. The endpoint converts these to public-only keys before serving.
Multiple Keys: During key rotation, multiple keys may be returned:
- New key: Recently created, used for signing new tokens
- Old keys: Still valid for verifying existing tokens
Returns:
- []JWK: All current keys (private + public key material)
- error: Database query error
Example:
// Serve JWKS endpoint
keys, _ := store.ListJWKS(ctx)
for _, dbKey := range keys {
key, _ := jwk.ParseKey(dbKey.KeyData)
pubKey, _ := key.PublicKey() // Extract public key only
jwksSet.AddKey(pubKey)
}
func (*DefaultJWTStore) StoreJWK ¶
func (s *DefaultJWTStore) StoreJWK(ctx context.Context, key jwk.Key, algorithm, use string, expiresAt *time.Time) error
StoreJWK persists a JWK to the database with optional expiration.
The key is JSON-serialized and stored with metadata for retrieval by algorithm and use type. This method is used during key rotation to add new keys while old keys remain valid until expiration.
Key Storage:
- Extracts kid (key ID) from the JWK (must be set before calling)
- Serializes entire key (public + private) to JSON
- Stores with algorithm and use metadata for filtering
- Records creation timestamp for ordering (newest = current)
- Optional expiration timestamp for key rotation cleanup
Key Rotation Workflow:
- Generate new key with unique kid
- Store with expiration = now + retention period
- Old keys remain valid until expiration (token verification)
- DeleteExpiredJWKS() removes old keys after retention period
Parameters:
- ctx: Request context
- key: JWK to store (must have kid set via SetKeyID)
- algorithm: Signing algorithm (e.g., "RS256")
- use: Key usage ("sig" or "enc")
- expiresAt: Optional expiration (nil = never expires)
Returns:
- error: Database error or JSON marshaling error
Example:
key, _ := rsa.GenerateKey(rand.Reader, 2048)
jwkKey, _ := jwk.FromRaw(key)
jwkKey.SetKeyID("key-2024-01")
expiry := time.Now().Add(30 * 24 * time.Hour)
err := store.StoreJWK(ctx, jwkKey, "RS256", "sig", &expiry)
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler manages HTTP handlers for the JWT plugin.
All handlers have been made private (lowercase) to encourage programmatic use of the underlying Plugin methods. This struct serves as a mounting point for the router.
func NewHandler ¶
NewHandler creates a new JWT HTTP handler.
type JWK ¶
type JWK struct {
// Kid is the Key ID (unique identifier for this key)
// Used in JWT header to identify which key was used for signing
// Format: timestamp-based or UUID
Kid string `json:"kid"`
// KeyData is the JSON-encoded JWK (includes public/private key material)
// Stored as BYTEA/BLOB in database
// For RSA: Contains n (modulus), e (exponent), d (private exponent)
KeyData []byte `json:"keyData"`
// Algorithm is the cryptographic algorithm (e.g., "RS256", "ES256")
// Currently only RS256 (RSA with SHA-256) is supported
Algorithm string `json:"algorithm"`
// Use indicates the key purpose:
// - "sig": Signature/verification (most common)
// - "enc": Encryption/decryption (future feature)
Use string `json:"use"`
// CreatedAt is when the key was generated
CreatedAt time.Time `json:"createdAt"`
// ExpiresAt is when the key should be deleted from storage
// Set to CreatedAt + KeyRetention duration
// nil means the key never expires (not recommended)
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
JWK represents a JSON Web Key stored in the database.
JWKs are cryptographic keys used for signing and verifying JWT tokens. They are stored in the database to support:
- Key rotation: Generate new keys periodically
- Key retention: Keep old keys for verifying existing tokens
- Multi-server: Share keys across multiple application instances
Key Lifecycle:
- Generate: Create RSA key pair (private + public)
- Store: Save to database with expiry time
- Use: Sign tokens with private key, verify with public key
- Rotate: Generate new key, keep old key for verification
- Expire: Delete keys after retention period
Database Schema:
CREATE TABLE jwk_keys ( kid VARCHAR(255) PRIMARY KEY, key_data BYTEA NOT NULL, algorithm VARCHAR(50) NOT NULL, use VARCHAR(50) NOT NULL, created_at TIMESTAMP NOT NULL, expires_at TIMESTAMP );
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin represents the JWT authentication plugin.
This plugin manages the complete JWT token lifecycle:
- Token generation (access + refresh pairs)
- Token validation (signature + expiry)
- Token refresh (new tokens from refresh token)
- Key rotation (periodic key generation)
- Key storage (database-backed JWKS)
- Token blacklist (Redis-backed revocation)
Architecture:
Components: - store: Database storage for JWK keys - handler: HTTP handlers for token endpoints - config: Token expiry and key rotation settings - redisClient: Token blacklist storage - sessionService: Integration with core authentication Keys: - accessTokenPrivateKey: Signs access tokens - accessTokenPublicKey: Verifies access tokens (exposed in JWKS) - refreshTokenPrivateKey: Signs refresh tokens - refreshTokenPublicKey: Verifies refresh tokens (not exposed) Token Lifecycle: 1. Generate: CreateTokenPair() → JWT signed with private key 2. Use: Client includes token in Authorization header 3. Verify: ValidateToken() → Check signature with public key 4. Refresh: RefreshTokens() → Generate new pair from refresh token 5. Revoke: BlacklistToken() → Add to Redis blacklist
func New ¶
New creates a new JWT authentication plugin.
Parameters:
- config: JWT configuration (token expiry, key rotation, etc.) Pass nil to use DefaultConfig()
- store: Custom JWK storage implementation Pass nil to use default SQL storage (recommended)
- dialect: Database dialect (postgres, mysql, sqlite) Optional, defaults to postgres if not provided
Example:
// Use default configuration
jwtPlugin := jwt.New(nil, nil, plugins.DialectPostgres)
// Custom configuration
config := &jwt.Config{
Issuer: "myapp.com",
AccessTokenExpiry: 1 * time.Hour,
RefreshTokenExpiry: 30 * 24 * time.Hour,
KeyRotationInterval: 7 * 24 * time.Hour,
}
jwtPlugin := jwt.New(config, nil, plugins.DialectPostgres)
func (*Plugin) BlacklistToken ¶
BlacklistToken adds a token to the revocation list.
func (*Plugin) CleanupExpiredKeys ¶
CleanupExpiredKeys removes expired keys from the database.
func (*Plugin) Dependencies ¶
func (p *Plugin) Dependencies() []plugins.Dependency
Dependencies returns external package dependencies.
func (*Plugin) Description ¶
Description returns a human-readable description.
func (*Plugin) GenerateTokenPair ¶
GenerateTokenPair creates access and refresh tokens for a user
func (*Plugin) GetMigrations ¶
GetMigrations returns the plugin migrations
func (*Plugin) GetSchemas ¶
GetSchemas returns all schemas for all supported dialects
func (*Plugin) Init ¶
Init initializes the JWT plugin and integrates with core authentication.
Initialization steps:
- Initialize JWK storage (default SQL store if not provided)
- Get Redis client from SessionService (for token blacklist)
- Validate database schema (ensure jwk_keys table exists)
- Initialize signing keys (load from DB or generate new)
- Create HTTP handler for token endpoints
- Start automatic key rotation (if configured)
Database Schema Validation:
The plugin validates that the jwk_keys table exists with required columns:
- kid (key ID, primary key)
- key_data (JWK JSON)
- algorithm (RS256, etc.)
- use (sig for signing, enc for encryption)
- created_at, expires_at
Key Initialization:
On first run, generates RSA key pairs for:
- Access tokens (public key exposed in JWKS)
- Refresh tokens (public key NOT exposed)
On subsequent runs, loads existing keys from database.
Parameters:
- ctx: Context for initialization (can be canceled)
- aegis: Framework instance providing database, services, etc.
Returns:
- error: Schema validation errors, key generation errors
func (*Plugin) LogoutAllSessions ¶
LogoutAllSessions invalidates all tokens for a user.
func (*Plugin) MountRoutes ¶
MountRoutes registers HTTP routes for JWT endpoints with appropriate middleware.
func (*Plugin) ProvidesAuthMethods ¶
ProvidesAuthMethods returns authentication methods provided.
func (*Plugin) RefreshTokens ¶
RefreshTokens handles token refresh mechanism.
func (*Plugin) RequiresTables ¶
RequiresTables returns tables this plugin manages.
func (*Plugin) RotateKeys ¶
RotateKeys rotates both access and refresh keys
func (*Plugin) StartKeyRotation ¶
StartKeyRotation begins periodic key rotation (only if Redis is configured).
func (*Plugin) ValidateToken ¶
ValidateToken checks the validity of a token.
type Provider ¶
type Provider interface {
// GenerateTokenPair creates an access token + refresh token for a user.
// Returns token strings and expiry times.
GenerateTokenPair(userID string) (*TokenPair, error)
// ValidateToken validates a JWT token and returns the claims.
// Checks signature, expiry, issuer, and token type.
ValidateToken(token string) (*Claims, error)
// RefreshTokens validates a refresh token and generates a new token pair.
// Old refresh token is invalidated (single-use refresh tokens).
RefreshTokens(refreshToken string) (*TokenPair, error)
// BlacklistToken adds a token to the blacklist (for logout/revocation).
// Requires Redis for distributed blacklist.
BlacklistToken(token string) error
}
Provider defines an interface for custom JWT token generation and validation.
Implement this interface to customize JWT token handling:
- Custom claims structure
- Custom signing algorithms
- Custom token validation logic
- Integration with external JWT services
The default implementation (Plugin) uses RSA signing with database-backed keys. Most applications don't need a custom provider.
type RefreshTokenRequest ¶
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token"`
}
RefreshTokenRequest represents a token refresh request.
type Store ¶
type Store interface {
// GetCurrentJWK retrieves the most recent active key for signing.
//
// This is used during token generation to get the private key for signing.
// Returns the newest non-expired key matching the algorithm and use.
//
// Parameters:
// - ctx: Context for cancellation and timeouts
// - algorithm: Key algorithm (e.g., "RS256")
// - use: Key use ("sig" for signing, "enc" for encryption)
//
// Returns:
// - jwk.Key: The most recent active key
// - error: Database errors or "key not found"
//
// Example:
//
// privateKey, err := store.GetCurrentJWK(ctx, "RS256", "sig")
// if err != nil {
// // No active key - generate new one
// }
GetCurrentJWK(ctx context.Context, algorithm, use string) (jwk.Key, error)
// StoreJWK persists a JWK to the database with expiry.
//
// This is used during:
// - Initial key generation (plugin initialization)
// - Key rotation (periodic background job)
//
// The key is stored with an expiry time for automatic cleanup.
// Expired keys are kept to verify old tokens until cleanup.
//
// Parameters:
// - ctx: Context for cancellation and timeouts
// - key: JWK to store (includes private/public key material)
// - algorithm: Key algorithm (e.g., "RS256")
// - use: Key use ("sig" or "enc")
// - expiresAt: When to delete this key (nil = never expires)
//
// Returns:
// - error: Database errors or duplicate key ID
//
// Example:
//
// expiresAt := time.Now().Add(30 * 24 * time.Hour)
// err := store.StoreJWK(ctx, privateKey, "RS256", "sig", &expiresAt)
StoreJWK(ctx context.Context, key jwk.Key, algorithm, use string, expiresAt *time.Time) error
// DeleteExpiredJWKS removes all expired keys from storage.
//
// This is called periodically to clean up old keys that are no longer needed.
// Only deletes keys where:
// - expires_at IS NOT NULL (keys with expiry set)
// - expires_at < NOW() (expiry time has passed)
//
// Note: Keys are kept past their "active" period to verify old tokens.
// The expiry should be set to created_at + KeyRetention duration.
//
// Parameters:
// - ctx: Context for cancellation and timeouts
//
// Returns:
// - error: Database errors
//
// Example:
//
// if err := store.DeleteExpiredJWKS(ctx); err != nil {
// log.Printf("Failed to cleanup expired keys: %v", err)
// }
DeleteExpiredJWKS(ctx context.Context) error
// ListJWKS returns all active (non-expired) keys.
//
// This is used by:
// - JWKS endpoint: Expose public keys for token verification
// - Token validation: Find key by kid (key ID) for signature verification
//
// Only returns keys where:
// - expires_at IS NULL (never expires), OR
// - expires_at > NOW() (not yet expired)
//
// Parameters:
// - ctx: Context for cancellation and timeouts
//
// Returns:
// - []JWK: List of active keys (includes both public and private keys)
// - error: Database errors
//
// Example:
//
// keys, err := store.ListJWKS(ctx)
// for _, key := range keys {
// fmt.Printf("Key: %s, Algorithm: %s\n", key.Kid, key.Algorithm)
// }
ListJWKS(ctx context.Context) ([]JWK, error)
}
Store defines the interface for JWT token and key storage operations.
This interface abstracts database operations for JWK (JSON Web Key) storage, allowing different storage backends:
- SQL databases: PostgreSQL, MySQL, SQLite (default)
- NoSQL databases: MongoDB, DynamoDB (custom implementation)
- Cloud key vaults: AWS KMS, Google Cloud KMS (custom implementation)
The default implementation (DefaultStore) uses SQL with sqlc-generated queries.
Key Storage Requirements:
- Persistence: Keys must survive application restarts
- Multi-server: Keys must be shared across application instances
- Key rotation: Support storing multiple active keys
- Key expiry: Automatic cleanup of expired keys
Thread Safety:
Implementations must be thread-safe for concurrent access from:
- Token generation requests (read current key)
- Key rotation background job (write new keys)
- Token validation requests (read all active keys)
- JWKS endpoint requests (list public keys)
type TokenPair ¶
type TokenPair struct {
// AccessToken is the JWT access token (use for API requests)
AccessToken string `json:"access_token"`
// AccessExpiry is when the access token expires (UTC)
AccessExpiry time.Time `json:"access_expiry"`
// RefreshToken is the JWT refresh token (use to get new access token)
RefreshToken string `json:"refresh_token"`
// RefreshExpiry is when the refresh token expires (UTC)
RefreshExpiry time.Time `json:"refresh_expiry"`
}
TokenPair represents a complete set of access and refresh tokens.
This is returned by token generation and refresh endpoints. Clients should:
- Store access token for API requests (short-lived)
- Store refresh token securely for obtaining new access tokens (long-lived)
- Use refresh token before access token expires
Example response:
{
"access_token": "eyJhbGc...",
"access_expiry": "2024-01-01T12:15:00Z",
"refresh_token": "eyJhbGc...",
"refresh_expiry": "2024-01-08T12:00:00Z"
}
type TokenRequest ¶
type TokenRequest struct {
UserID string `json:"user_id"`
}
TokenRequest represents a request to get a JWT token.