jwt

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 23, 2026 License: MIT Imports: 23 Imported by: 0

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

View Source
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"
)
View Source
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"

	// SchemaAccessToken is the OpenAPI schema name for access token only responses.
	// Response Body:
	//   {
	//     "access_token": "eyJhbGc...",
	//     "access_expiry": "2024-01-01T12:15:00Z"
	//   }
	//
	// Used by:
	//   - POST /getAccessToken (success response)
	SchemaAccessToken = "AccessToken"

	// SchemaJWKS is the OpenAPI schema name for JWKS responses.
	// Response Body:
	//   {
	//     "keys": [
	//       {
	//         "kty": "RSA",
	//         "use": "sig",
	//         "kid": "access-1234567890",
	//         "n": "...",
	//         "e": "AQAB"
	//       }
	//     ]
	//   }
	//
	// Used by:
	//   - GET /.well-known/jwks.json
	//   - GET /jwks
	SchemaJWKS = "JWKS"
)

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

func GetMigrations(dialect plugins.Dialect) ([]plugins.Migration, error)

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:

  1. Load schema.sql as version 001 with no down migration
  2. Scan migrations/<dialect>/ for version 002+ files
  3. Parse version number and type (up/down) from filename
  4. Group up/down migrations by version
  5. Sort by version number ascending
  6. 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

func GetSchema(dialect plugins.Dialect) (*plugins.Schema, error)

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 AccessToken

type AccessToken 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"`
}

AccessToken represents a single access token response.

This is returned by the /getAccessToken endpoint when only an access token is needed (without a refresh token).

Example response:

{
  "access_token": "eyJhbGc...",
  "access_expiry": "2024-01-01T12:15:00Z"
}

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:

  1. Generate new key with unique kid
  2. Store with expiration = now + retention period
  3. Old keys remain valid until expiration (token verification)
  4. 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

func NewHandler(plugin *Plugin) *Handler

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:

  1. Generate: Create RSA key pair (private + public)
  2. Store: Save to database with expiry time
  3. Use: Sign tokens with private key, verify with public key
  4. Rotate: Generate new key, keep old key for verification
  5. 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 JWKS

type JWKS struct {
	// Keys is the array of public keys
	Keys []map[string]any `json:"keys"`
}

JWKS represents a JSON Web Key Set response.

This is returned by the /.well-known/jwks.json endpoint and contains the public keys used to verify JWT signatures.

Example response:

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "access-1234567890",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

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

func New(config *Config, store Store, dialect ...plugins.Dialect) *Plugin

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

func (p *Plugin) BlacklistToken(tokenStr string) error

BlacklistToken adds a token to the revocation list.

func (*Plugin) CleanupExpiredKeys

func (p *Plugin) CleanupExpiredKeys(ctx context.Context) error

CleanupExpiredKeys removes expired keys from the database.

func (*Plugin) Dependencies

func (p *Plugin) Dependencies() []plugins.Dependency

Dependencies returns external package dependencies.

func (*Plugin) Description

func (p *Plugin) Description() string

Description returns a human-readable description.

func (*Plugin) GenerateTokenPair

func (p *Plugin) GenerateTokenPair(userID string) (*TokenPair, error)

GenerateTokenPair creates access and refresh tokens for a user

func (*Plugin) GetMigrations

func (p *Plugin) GetMigrations() []plugins.Migration

GetMigrations returns the plugin migrations

func (*Plugin) GetSchemas

func (p *Plugin) GetSchemas() []plugins.Schema

GetSchemas returns all schemas for all supported dialects

func (*Plugin) Init

func (p *Plugin) Init(ctx context.Context, aegis plugins.Aegis) error

Init initializes the JWT plugin and integrates with core authentication.

Initialization steps:

  1. Initialize JWK storage (default SQL store if not provided)
  2. Get Redis client from SessionService (for token blacklist)
  3. Validate database schema (ensure jwk_keys table exists)
  4. Initialize signing keys (load from DB or generate new)
  5. Create HTTP handler for token endpoints
  6. 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) Logout

func (p *Plugin) Logout(tokenStr string) error

Logout is a programmatic alias for BlacklistToken.

func (*Plugin) LogoutAllSessions

func (p *Plugin) LogoutAllSessions(userID string) error

LogoutAllSessions invalidates all tokens for a user.

func (*Plugin) MountRoutes

func (p *Plugin) MountRoutes(router router.Router, basePath string)

MountRoutes registers HTTP routes for JWT endpoints with appropriate middleware.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin identifier.

func (*Plugin) ProvidesAuthMethods

func (p *Plugin) ProvidesAuthMethods() []string

ProvidesAuthMethods returns authentication methods provided.

func (*Plugin) RefreshTokens

func (p *Plugin) RefreshTokens(refreshToken string) (*TokenPair, error)

RefreshTokens handles token refresh mechanism.

func (*Plugin) RequiresTables

func (p *Plugin) RequiresTables() []string

RequiresTables returns tables this plugin manages.

func (*Plugin) RotateKeys

func (p *Plugin) RotateKeys(ctx context.Context) error

RotateKeys rotates both access and refresh keys

func (*Plugin) StartKeyRotation

func (p *Plugin) StartKeyRotation(ctx context.Context)

StartKeyRotation begins periodic key rotation (only if Redis is configured).

func (*Plugin) ValidateToken

func (p *Plugin) ValidateToken(tokenStr string) (*Claims, error)

ValidateToken checks the validity of a token.

func (*Plugin) Version

func (p *Plugin) Version() string

Version returns the plugin version.

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.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL