blueprint.provider.jwtprovider¶
Blueprint JWT provider for comprehensive JSON Web Token authentication and management.
Overview¶
The JWT provider offers a complete solution for managing JWT tokens in Go applications. It supports multiple signing algorithms, token revocation, refresh capabilities, and secure key management.
Key features:
- Multiple signing algorithms (HMAC, RSA, ECDSA, EdDSA)
- Token revocation system with pluggable backends
- User token tracking and session management
- Configurable concurrent session limits
- Token refresh functionality
- Secure key management with Blueprint's credential system
- Comprehensive validation options
- Thread-safe operations
Supported Signing Algorithms¶
Algorithm | Type | Security | Key Requirements | Recommended |
---|---|---|---|---|
HS256 | HMAC | High | Shared secret | Yes |
HS384 | HMAC | High | Shared secret | Yes |
HS512 | HMAC | High | Shared secret | Yes |
RS256 | RSA | High | RSA key pair | Yes |
RS384 | RSA | High | RSA key pair | Yes |
RS512 | RSA | High | RSA key pair | Yes |
ES256 | ECDSA | High | ECDSA key pair | Yes |
ES384 | ECDSA | High | ECDSA key pair | Yes |
ES512 | ECDSA | High | ECDSA key pair | Yes |
EdDSA | EdDSA | High | Ed25519 key pair | Yes |
Configuration¶
Basic Configuration¶
type JWTConfig struct {
SigningKey *secure.DefaultCredentialConfig `json:"signingKey,omitempty"` // For HMAC algorithms
PrivateKey *secure.KeyConfig `json:"privateKey,omitempty"` // For asymmetric algorithms
PublicKey *secure.KeyConfig `json:"publicKey,omitempty"` // For asymmetric algorithms
SigningAlgorithm string `json:"signingAlgorithm"` // Algorithm to use
ExpirationSeconds int `json:"expirationSeconds"` // Token expiration
Issuer string `json:"issuer"` // Token issuer
Audience string `json:"audience"` // Token audience
KeyID string `json:"keyID"` // Key ID for JWKS
MaxTokenSize int `json:"maxTokenSize,omitempty"` // Maximum token size (bytes)
RequireIssuer bool `json:"requireIssuer"` // Enforce issuer validation
RequireAudience bool `json:"requireAudience"` // Enforce audience validation
// User Token Tracking
TrackUserTokens bool `json:"trackUserTokens"` // Enable user token tracking
MaxUserSessions int `json:"maxUserSessions,omitempty"` // Max concurrent sessions per user (0 = unlimited)
}
Default Values¶
const (
DefaultTTL = time.Second * 86400 // 1 day
DefaultIssuer = "blueprint"
DefaultAudience = "api"
)
Basic Usage¶
HMAC Algorithms (HS256/HS384/HS512)¶
package main
import (
"fmt"
"log"
"github.com/oddbit-project/blueprint/provider/jwtprovider"
"github.com/oddbit-project/blueprint/crypt/secure"
)
func main() {
// Create configuration for HMAC signing
config := jwtprovider.NewJWTConfig()
config.SigningAlgorithm = jwtprovider.HS256
config.ExpirationSeconds = 3600 // 1 hour
config.Issuer = "my-app"
config.Audience = "api"
config.RequireIssuer = true
config.RequireAudience = true
// Set up signing key
signingKey, err := secure.GenerateKey()
if err != nil {
log.Fatal(err)
}
config.SigningKey = &secure.DefaultCredentialConfig{
Key: signingKey,
}
// Create JWT provider
provider, err := jwtprovider.NewProvider(config)
if err != nil {
log.Fatal(err)
}
// Generate a token
customData := map[string]any{
"role": "admin",
"permissions": []string{"read", "write"},
}
token, err := provider.GenerateToken("user123", customData)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Generated token: %s\n", token)
// Parse and validate the token
claims, err := provider.ParseToken(token)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Subject: %s\n", claims.Subject)
fmt.Printf("Custom data: %v\n", claims.Data)
}
RSA Algorithms (RS256/RS384/RS512)¶
package main
import (
"fmt"
"log"
"github.com/oddbit-project/blueprint/provider/jwtprovider"
"github.com/oddbit-project/blueprint/crypt/secure"
)
func main() {
// Create configuration for RSA signing
config := jwtprovider.NewJWTConfig()
config.SigningAlgorithm = jwtprovider.RS256
config.ExpirationSeconds = 3600
config.Issuer = "my-app"
config.Audience = "api"
config.KeyID = "key-1" // For JWKS support
// Set up RSA key pair (PEM encoded PKCS#8)
privateKeyPEM := `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7...
-----END PRIVATE KEY-----`
publicKeyPEM := `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu8...
-----END PUBLIC KEY-----`
// Configure private key
privateKeyConfig, err := secure.NewKeyConfig([]byte(privateKeyPEM))
if err != nil {
log.Fatal(err)
}
config.PrivateKey = privateKeyConfig
// Configure public key
publicKeyConfig, err := secure.NewKeyConfig([]byte(publicKeyPEM))
if err != nil {
log.Fatal(err)
}
config.PublicKey = publicKeyConfig
// Create provider
provider, err := jwtprovider.NewProvider(config)
if err != nil {
log.Fatal(err)
}
// Generate and parse tokens same as HMAC example...
}
Token Revocation¶
Setting Up Revocation¶
// Create in-memory revocation backend
revocationBackend := jwtprovider.NewMemoryRevocationBackend()
// Create revocation manager
revocationManager := jwtprovider.NewRevocationManager(revocationBackend)
// Create provider with revocation support
provider, err := jwtprovider.NewProvider(config,
jwtprovider.WithRevocationManager(revocationManager))
if err != nil {
log.Fatal(err)
}
Revoking Tokens¶
// Revoke a specific token
err := provider.RevokeToken(tokenString)
if err != nil {
log.Fatal(err)
}
// Revoke by token ID (from claims)
claims, err := provider.ParseToken(tokenString)
if err != nil {
log.Fatal(err)
}
err = provider.RevokeTokenByID(claims.ID, claims.ExpiresAt.Time)
if err != nil {
log.Fatal(err)
}
// Check if token is revoked
isRevoked := provider.IsTokenRevoked(claims.ID)
fmt.Printf("Token revoked: %v\n", isRevoked)
Custom Revocation Backend¶
// Implement custom revocation backend
type DatabaseRevocationBackend struct {
db *sql.DB
}
func (d *DatabaseRevocationBackend) RevokeToken(tokenID string, expiresAt time.Time) error {
_, err := d.db.Exec("INSERT INTO revoked_tokens (token_id, expires_at, revoked_at) VALUES (?, ?, ?)",
tokenID, expiresAt, time.Now())
return err
}
func (d *DatabaseRevocationBackend) IsTokenRevoked(tokenID string) bool {
var count int
err := d.db.QueryRow("SELECT COUNT(*) FROM revoked_tokens WHERE token_id = ? AND expires_at > ?",
tokenID, time.Now()).Scan(&count)
return err == nil && count > 0
}
// Implement other RevocationBackend methods...
// Use custom backend
customBackend := &DatabaseRevocationBackend{db: yourDB}
revocationManager := jwtprovider.NewRevocationManager(customBackend)
User Token Tracking¶
The JWT provider supports comprehensive user token tracking for session management, security auditing, and bulk operations.
Enabling Token Tracking¶
// Configure provider with user token tracking
config := jwtprovider.NewJWTConfig()
config.TrackUserTokens = true // Enable token tracking
config.MaxUserSessions = 5 // Limit concurrent sessions (0 = unlimited)
// Create revocation manager (required for tracking)
revocationMgr := jwtprovider.NewRevocationManager(
jwtprovider.NewMemoryRevocationBackend(),
)
provider, err := jwtprovider.NewProvider(config,
jwtprovider.WithRevocationManager(revocationMgr))
if err != nil {
log.Fatal(err)
}
defer revocationMgr.Close()
Session Management¶
userID := "user123"
// Generate token (automatically tracked when enabled)
token, err := provider.GenerateToken(userID, map[string]any{
"role": "admin",
})
// Handle session limit exceeded
if err == jwtprovider.ErrMaxSessionsExceeded {
return fmt.Errorf("maximum concurrent sessions reached")
}
// Check active session count
sessionCount := provider.GetUserSessionCount(userID)
fmt.Printf("Active sessions: %d\n", sessionCount)
// Get all active tokens for user
activeTokens, err := provider.GetActiveUserTokens(userID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User has %d active tokens\n", len(activeTokens))
for i, tokenID := range activeTokens {
fmt.Printf(" %d. %s\n", i+1, tokenID)
}
Bulk Token Operations¶
// Revoke all tokens for a user (e.g., on password change)
err := provider.RevokeAllUserTokens(userID)
if err != nil {
log.Fatal(err)
}
// Useful for security events:
// - Password reset
// - Account compromise
// - Security policy changes
// - User logout from all devices
Complete Session Management Example¶
package main
import (
"fmt"
"log"
"github.com/oddbit-project/blueprint/provider/jwtprovider"
)
func main() {
// Setup provider with session tracking
config := jwtprovider.NewJWTConfig()
config.TrackUserTokens = true
config.MaxUserSessions = 3
// Set up signing key
signingKey := []byte("your-secret-key-32-bytes-minimum")
config.CfgSigningKey = &secure.DefaultCredentialConfig{
Password: string(signingKey),
}
revocationMgr := jwtprovider.NewRevocationManager(
jwtprovider.NewMemoryRevocationBackend(),
)
provider, err := jwtprovider.NewProvider(config,
jwtprovider.WithRevocationManager(revocationMgr))
if err != nil {
log.Fatal(err)
}
defer revocationMgr.Close()
userID := "user123"
// Generate multiple tokens
fmt.Printf("Generating tokens for user %s...\n", userID)
var tokens []string
for i := 1; i <= 4; i++ {
token, err := provider.GenerateToken(userID, map[string]any{
"session_id": fmt.Sprintf("session_%d", i),
})
if err != nil {
if err == jwtprovider.ErrMaxSessionsExceeded {
fmt.Printf("Token %d: FAILED - session limit exceeded\n", i)
continue
}
log.Fatal(err)
}
tokens = append(tokens, token)
fmt.Printf("Token %d: SUCCESS\n", i)
count := provider.GetUserSessionCount(userID)
fmt.Printf(" Current sessions: %d\n", count)
}
// Security event: revoke all user tokens
fmt.Println("\nSecurity event: revoking all user tokens...")
err = provider.RevokeAllUserTokens(userID)
if err != nil {
log.Fatal(err)
}
// Verify all tokens are revoked
fmt.Println("Verifying token revocation...")
for i, token := range tokens {
_, err := provider.ParseToken(token)
if err != nil {
fmt.Printf("Token %d: REVOKED ✓\n", i+1)
} else {
fmt.Printf("Token %d: STILL VALID ✗\n", i+1)
}
}
finalCount := provider.GetUserSessionCount(userID)
fmt.Printf("\nFinal session count: %d\n", finalCount)
}
Security Benefits¶
Session Control: - Prevent credential sharing by limiting concurrent sessions - Automatically handle session limits during token generation - Track all active sessions per user
Security Response: - Quickly revoke all user tokens during security incidents - Audit trail of token issuance and revocation - Detect unusual session patterns
Memory Management: - Automatic cleanup of expired token metadata - Efficient storage with O(1) lookup performance - Background cleanup prevents memory leaks
Token Metadata¶
When tracking is enabled, the provider stores comprehensive metadata:
type TokenMetadata struct {
TokenID string `json:"tokenId"`
UserID string `json:"userId"`
IssuedAt time.Time `json:"issuedAt"`
ExpiresAt time.Time `json:"expiresAt"`
ClientIP string `json:"clientIP,omitempty"` // For future use
UserAgent string `json:"userAgent,omitempty"` // For future use
}
Error Handling¶
const (
ErrMaxSessionsExceeded = "maximum concurrent sessions exceeded"
)
// Handle session limits
token, err := provider.GenerateToken(userID, data)
if err != nil {
switch err {
case jwtprovider.ErrMaxSessionsExceeded:
// Inform user about session limit
return "Too many active sessions. Please log out from other devices."
case jwtprovider.ErrNoRevocationManager:
// Tracking requires revocation manager
log.Error("Token tracking requires revocation manager")
default:
log.Errorf("Token generation failed: %v", err)
}
}
Performance Considerations¶
Memory Usage: - O(n) memory where n = number of active tokens - Automatic cleanup of expired metadata - Configurable cleanup intervals
Lookup Performance: - O(1) token revocation checks - O(1) user session count queries - O(k) user token retrieval where k = user's token count
Concurrency: - Thread-safe operations with read-write locks - Minimal lock contention for read operations - Background cleanup doesn't block operations
Token Refresh¶
// Refresh an existing token
newToken, err := provider.Refresh(oldTokenString)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Refreshed token: %s\n", newToken)
Claims Structure¶
type Claims struct {
jwt.RegisteredClaims // Standard JWT claims
Data map[string]any `json:"data,omitempty"` // Custom data
}
// Standard claims include:
// - Subject (sub)
// - Issuer (iss)
// - Audience (aud)
// - ExpiresAt (exp)
// - NotBefore (nbf)
// - IssuedAt (iat)
// - ID (jti)
Error Handling¶
const (
ErrInvalidSigningAlgorithm = "JWT signing algorithm is invalid"
ErrInvalidToken = "invalid token"
ErrTokenExpired = "token has expired"
ErrMissingIssuer = "issuer validation failed"
ErrMissingAudience = "audience validation failed"
ErrNoRevocationManager = "revocation manager not available"
ErrTokenAlreadyRevoked = "token is already revoked"
ErrInvalidTokenID = "invalid token ID"
ErrMaxSessionsExceeded = "maximum concurrent sessions exceeded"
ErrTokenTooLarge = "token too large"
ErrTokenParsingTimeout = "token parsing timeout"
)
// Example error handling
token, err := provider.GenerateToken("user123", data)
if err != nil {
switch err {
case jwtprovider.ErrInvalidSigningAlgorithm:
log.Fatal("Invalid signing algorithm configured")
case jwtprovider.ErrMaxSessionsExceeded:
log.Printf("User has too many concurrent sessions")
// Handle session limit - maybe offer to logout other devices
case jwtprovider.ErrTokenTooLarge:
log.Printf("Token payload too large, reduce custom claims")
default:
log.Fatalf("Token generation failed: %v", err)
}
}
Integration with HTTP Server¶
Middleware Integration¶
package main
import (
"github.com/gin-gonic/gin"
"github.com/oddbit-project/blueprint/log"
"github.com/oddbit-project/blueprint/provider/httpserver"
"github.com/oddbit-project/blueprint/provider/httpserver/auth"
"github.com/oddbit-project/blueprint/provider/jwtprovider"
"net/http"
"os"
)
func main() {
// Configure logger
log.Configure(log.NewDefaultConfig())
logger := log.New("jwt-demo")
// Create server config
serverConfig := httpserver.NewServerConfig()
serverConfig.Host = "localhost"
serverConfig.Port = 8080
serverConfig.Debug = true
// Create HTTP server
server, err := httpserver.NewServer(serverConfig, logger)
if err != nil {
logger.Fatal(err, "could not create server")
os.Exit(1)
}
// Set up JWT provider...
provider, err := jwtprovider.NewProvider(config)
if err != nil {
logger.Fatal(err, "could not create JWT provider")
os.Exit(1)
}
// Public route for login
server.Route().POST("/login", loginHandler(provider))
// Apply JWT authentication to all subsequent routes
server.UseAuth(auth.NewAuthJWT(provider))
// Protected routes
server.Route().GET("/api/profile", profileHandler)
// Start server
if err := server.Start(); err != nil {
logger.Fatal(err, "failed to start server")
}
}
func profileHandler(c *gin.Context) {
// Get claims using Blueprint auth helper
claims, ok := auth.GetClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"user": claims})
}
func loginHandler(provider jwtprovider.JWTProvider) gin.HandlerFunc {
return func(c *gin.Context) {
// Simple authentication
var credentials struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&credentials); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// Dummy validation
if credentials.Username != "admin" || credentials.Password != "secret" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
token, err := provider.GenerateToken(credentials.Username, map[string]any{
"role": "admin",
})
if err != nil {
// Handle session limits gracefully
if err == jwtprovider.ErrMaxSessionsExceeded {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Too many active sessions",
"message": "Please log out from other devices",
"active_sessions": provider.GetUserSessionCount(credentials.Username),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"active_sessions": provider.GetUserSessionCount(credentials.Username),
})
}
}
Security Best Practices¶
Key Management¶
- Use strong keys: Minimum 256 bits for HMAC, 2048 bits for RSA
- Secure storage: Use Blueprint's secure credential system
- Key rotation: Implement regular key rotation
- Separate keys: Use different keys for different environments
Token Security¶
- Short expiration: Use appropriate token lifetimes (15-60 minutes)
- Revocation: Implement token revocation for security events
- Refresh tokens: Use refresh tokens for long-lived sessions
- Secure transmission: Always use HTTPS
- Validation: Validate all claims (issuer, audience, expiration)
- Session limits: Configure reasonable concurrent session limits
- Input validation: Enable token size limits and parsing timeouts
- User tracking: Enable token tracking for security audit trails
Configuration Security¶
// Production configuration example
config := jwtprovider.NewJWTConfig()
config.SigningAlgorithm = jwtprovider.RS256 // Asymmetric algorithm
config.ExpirationSeconds = 900 // 15 minutes
config.Issuer = "my-production-app"
config.Audience = "api-production"
config.RequireIssuer = true // Enforce validation
config.RequireAudience = true // Enforce validation
config.KeyID = "prod-key-2024-01" // Key identification
config.MaxTokenSize = 8192 // 8KB token limit
config.TrackUserTokens = true // Enable session tracking
config.MaxUserSessions = 5 // Reasonable session limit
JWKS (JSON Web Key Set) Support¶
// Configure with Key ID for JWKS
config.KeyID = "my-key-1"
// The provider will include the "kid" header in generated tokens
// This allows for key rotation and multiple simultaneous keys
Performance Considerations¶
- Algorithm choice: HMAC algorithms are faster than asymmetric
- Key caching: Keys are cached after first use
- Revocation backend: In-memory backend is fastest, database backend for persistence
- Token size: Minimize custom claims to reduce token size
- Validation caching: Consider caching validation results for frequently accessed tokens
- Token tracking: Adds minimal overhead (O(1) operations, automatic cleanup)
- Memory usage: User tracking requires O(n) memory where n = active tokens
- Session limits: Early validation prevents unnecessary token generation
Complete Example Application¶
package main
import (
"github.com/gin-gonic/gin"
"github.com/oddbit-project/blueprint/log"
"github.com/oddbit-project/blueprint/provider/httpserver"
"github.com/oddbit-project/blueprint/provider/httpserver/auth"
"github.com/oddbit-project/blueprint/provider/jwtprovider"
"github.com/oddbit-project/blueprint/crypt/secure"
"net/http"
"os"
)
func main() {
// Configure logger
log.Configure(log.NewDefaultConfig())
logger := log.New("jwt-demo")
// Create server config
serverConfig := httpserver.NewServerConfig()
serverConfig.Host = "localhost"
serverConfig.Port = 8080
serverConfig.Debug = true
// Create HTTP server
server, err := httpserver.NewServer(serverConfig, logger)
if err != nil {
logger.Fatal(err, "could not create server")
os.Exit(1)
}
// Configure JWT provider using proper constructor
config := jwtprovider.NewJWTConfig()
config.SigningAlgorithm = jwtprovider.HS256
config.ExpirationSeconds = 3600
config.Issuer = "demo-app"
config.Audience = "api"
config.RequireIssuer = true
config.RequireAudience = true
config.TrackUserTokens = true // Enable session tracking
config.MaxUserSessions = 3 // Limit concurrent sessions
// Generate signing key
signingKey, err := secure.GenerateKey()
if err != nil {
logger.Fatal(err, "could not generate signing key")
os.Exit(1)
}
config.CfgSigningKey = &secure.DefaultCredentialConfig{Password: string(signingKey)}
// Create provider with revocation
revocationBackend := jwtprovider.NewMemoryRevocationBackend()
revocationManager := jwtprovider.NewRevocationManager(revocationBackend)
provider, err := jwtprovider.NewProvider(config,
jwtprovider.WithRevocationManager(revocationManager))
if err != nil {
logger.Fatal(err, "could not create JWT provider")
os.Exit(1)
}
// Public login route
server.Route().POST("/login", func(c *gin.Context) {
// Simple authentication (replace with real auth)
var loginReq struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&loginReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
if loginReq.Username == "admin" && loginReq.Password == "secret" {
token, err := provider.GenerateToken(loginReq.Username, map[string]any{
"role": "admin",
})
if err != nil {
// Handle session limits
if err == jwtprovider.ErrMaxSessionsExceeded {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Too many active sessions",
"active_sessions": provider.GetUserSessionCount(loginReq.Username),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"active_sessions": provider.GetUserSessionCount(loginReq.Username),
})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
}
})
// Apply JWT authentication to protected routes
server.UseAuth(auth.NewAuthJWT(provider))
server.Route().POST("/logout", func(c *gin.Context) {
claims, ok := auth.GetClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
err := provider.RevokeTokenByID(claims.ID, claims.ExpiresAt.Time)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to logout"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
})
server.Route().GET("/api/profile", func(c *gin.Context) {
claims, ok := auth.GetClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"user": claims})
})
// Session management endpoints
server.Route().GET("/api/sessions", func(c *gin.Context) {
claims, ok := auth.GetClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
activeTokens, err := provider.GetActiveUserTokens(claims.Subject)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get sessions"})
return
}
c.JSON(http.StatusOK, gin.H{
"active_sessions": len(activeTokens),
"tokens": activeTokens,
})
})
server.Route().DELETE("/api/sessions", func(c *gin.Context) {
claims, ok := auth.GetClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
err := provider.RevokeAllUserTokens(claims.Subject)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke sessions"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "All sessions revoked"})
})
// Start server
logger.Info("Server starting on http://localhost:8080")
logger.Info("Available endpoints:")
logger.Info(" POST /login - Authenticate (public)")
logger.Info(" POST /logout - Logout (protected)")
logger.Info(" GET /api/profile - User profile (protected)")
logger.Info(" GET /api/sessions - List sessions (protected)")
logger.Info(" DELETE /api/sessions - Revoke all sessions (protected)")
if err := server.Start(); err != nil {
logger.Fatal(err, "failed to start server")
}
}