Documentation
¶
Overview ¶
Package middleware provides production-ready HTTP middleware for Signet authentication.
Signet replaces bearer tokens with cryptographic proof-of-possession, implementing two-step verification that ensures both token validity and request authenticity.
Security Properties ¶
The middleware enforces multiple security layers:
- Two-step cryptographic verification (master→ephemeral→request)
- Per-request replay prevention via nonce tracking
- Time-bound tokens with configurable validity windows
- Clock skew tolerance for distributed systems
- Purpose-specific token validation
Basic Usage ¶
Create middleware with minimal configuration:
auth := middleware.SignetMiddleware(
middleware.WithMasterKey(masterPublicKey),
)
protected := auth(yourHandler)
Distributed Systems ¶
For multi-instance deployments, use shared storage:
auth := middleware.SignetMiddleware(
middleware.WithTokenStore(redisTokenStore),
middleware.WithNonceStore(redisNonceStore),
middleware.WithKeyProvider(dynamicKeyProvider),
)
Authentication Context ¶
Verified requests include authentication details:
func handler(w http.ResponseWriter, r *http.Request) {
authCtx, _ := middleware.GetAuthContext(r)
log.Printf("Token: %s, Purpose: %s", authCtx.TokenID, authCtx.Purpose)
}
Extensibility ¶
The middleware is built on clean interfaces:
- TokenStore: Token storage and retrieval
- NonceStore: Replay prevention
- KeyProvider: Master key management
- RequestBuilder: Custom canonicalization
- Logger & Metrics: Observability integration
Thread Safety ¶
All provided implementations are thread-safe and suitable for concurrent use. Custom implementations should ensure proper synchronization.
Error Handling ¶
The middleware provides consistent error codes for client handling:
- MISSING_PROOF: No authentication header
- INVALID_PROOF: Malformed header
- TOKEN_EXPIRED: Token past validity
- REPLAY_DETECTED: Request already processed
- INVALID_SIGNATURE: Verification failed
For detailed examples, see the example_test.go file.
Package middleware provides production-ready HTTP middleware for Signet authentication. This middleware implements two-step cryptographic verification (master→ephemeral→request) with configurable backends for token and nonce storage.
Example (CustomCanonical) ¶
Example_customCanonical demonstrates custom request canonicalization
package main
import (
"crypto/ed25519"
"fmt"
"net/http"
"github.com/agentic-research/signet/pkg/http/header"
"github.com/agentic-research/signet/pkg/http/middleware"
)
// CustomRequestBuilder implements custom request canonicalization
type CustomRequestBuilder struct{}
func (b *CustomRequestBuilder) Build(r *http.Request, proof *header.SignetProof) ([]byte, error) {
canonical := fmt.Sprintf("%s|%s|%s|%d",
r.Method,
r.URL.String(),
r.Header.Get("X-Request-ID"),
proof.Timestamp,
)
return []byte(canonical), nil
}
func main() {
masterPub, _, _ := ed25519.GenerateKey(nil)
auth, err := middleware.SignetMiddleware(
middleware.WithMasterKey(masterPub),
middleware.WithRequestBuilder(&CustomRequestBuilder{}),
)
if err != nil {
panic(err) // In production, handle this error gracefully
}
handler := auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Custom canonical request"))
}))
_ = http.ListenAndServe(":8080", handler)
}
Output:
Example (DistributedSetup) ¶
Example_distributedSetup demonstrates setup for distributed systems
package main
import (
"crypto/ed25519"
"log"
"net/http"
"time"
"github.com/agentic-research/signet/pkg/http/middleware"
)
func main() {
// Create Redis-backed stores for distributed deployments
// (assumes you have a Redis client implementation)
/*
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
tokenStore := middleware.NewRedisTokenStore(redisClient, "signet:tokens:")
nonceStore := middleware.NewRedisNonceStore(redisClient, "signet:nonces:")
*/
// For this example, we'll use memory stores
tokenStore := middleware.NewMemoryTokenStore()
nonceStore := middleware.NewMemoryNonceStore()
// Multi-issuer key provider
keyProvider := middleware.NewMultiKeyProvider()
pub1, _, _ := ed25519.GenerateKey(nil)
pub2, _, _ := ed25519.GenerateKey(nil)
keyProvider.AddKey("issuer1", pub1)
keyProvider.AddKey("issuer2", pub2)
// Create middleware with advanced configuration
auth, err := middleware.SignetMiddleware(
middleware.WithTokenStore(tokenStore),
middleware.WithNonceStore(nonceStore),
middleware.WithKeyProvider(keyProvider),
middleware.WithClockSkew(1*time.Minute),
middleware.WithJSONErrors(),
middleware.WithSkipPaths("/health", "/metrics"),
middleware.WithRequiredPurposes("api-access", "admin-access"),
)
if err != nil {
panic(err) // In production, handle this error gracefully
}
// Create your application handler
app := http.NewServeMux()
// Public endpoints (skipped by middleware)
app.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"status": "healthy"}`))
})
// Protected endpoints
app.Handle("/api/", auth(http.HandlerFunc(apiHandler)))
app.Handle("/admin/", auth(http.HandlerFunc(adminHandler)))
_ = http.ListenAndServe(":8080", app)
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
authCtx, _ := middleware.GetAuthContext(r)
log.Printf("API request from token %s with purpose %s", authCtx.TokenID, authCtx.Purpose)
_, _ = w.Write([]byte("API response"))
}
func adminHandler(w http.ResponseWriter, r *http.Request) {
authCtx, _ := middleware.GetAuthContext(r)
if authCtx.Purpose != "admin-access" {
http.Error(w, "Admin access required", http.StatusForbidden)
return
}
_, _ = w.Write([]byte("Admin response"))
}
Output:
Example (SimpleUsage) ¶
Example_simpleUsage demonstrates basic middleware setup with a static key
package main
import (
"crypto/ed25519"
"net/http"
"time"
"github.com/agentic-research/signet/pkg/http/middleware"
)
func main() {
// Generate or load your master key
masterPub, _, _ := ed25519.GenerateKey(nil)
// Create middleware with simple configuration
auth, err := middleware.SignetMiddleware(
middleware.WithMasterKey(masterPub),
middleware.WithClockSkew(30*time.Second),
)
if err != nil {
panic(err) // In production, handle this error gracefully
}
// Apply to your handler
handler := auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get authentication context
authCtx, ok := middleware.GetAuthContext(r)
if !ok {
// This shouldn't happen if middleware is properly configured
http.Error(w, "No auth context", http.StatusInternalServerError)
return
}
// Access authenticated request information
_, _ = w.Write([]byte("Hello, authenticated user! Token: " + authCtx.TokenID))
}))
// Use the handler
_ = http.ListenAndServe(":8080", handler)
}
Output:
Example (WithObservability) ¶
Example_withObservability demonstrates integration with logging and metrics
package main
import (
"crypto/ed25519"
"log"
"net/http"
"time"
"github.com/agentic-research/signet/pkg/http/middleware"
)
// CustomLogger implements middleware.Logger for integration with your logging system
type CustomLogger struct {
}
func (l *CustomLogger) Debug(msg string, args ...interface{}) {
log.Printf("[DEBUG] %s %v", msg, args)
}
func (l *CustomLogger) Info(msg string, args ...interface{}) {
log.Printf("[INFO] %s %v", msg, args)
}
func (l *CustomLogger) Warn(msg string, args ...interface{}) {
log.Printf("[WARN] %s %v", msg, args)
}
func (l *CustomLogger) Error(msg string, args ...interface{}) {
log.Printf("[ERROR] %s %v", msg, args)
}
// PrometheusMetrics implements middleware.Metrics for Prometheus integration
type PrometheusMetrics struct {
}
func (m *PrometheusMetrics) RecordAuthResult(result string, duration time.Duration) {
}
func (m *PrometheusMetrics) RecordTokenUsage(tokenID string, purpose string) {
}
func main() {
masterPub, _, _ := ed25519.GenerateKey(nil)
// Create middleware with observability
auth, err := middleware.SignetMiddleware(
middleware.WithMasterKey(masterPub),
middleware.WithLogger(&CustomLogger{}),
middleware.WithMetrics(&PrometheusMetrics{}),
)
if err != nil {
panic(err) // In production, handle this error gracefully
}
handler := auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Authenticated with observability"))
}))
_ = http.ListenAndServe(":8080", handler)
}
Output:
Index ¶
- Variables
- func SignetMiddleware(opts ...Option) (func(http.Handler) http.Handler, error)
- type AuthContext
- type Config
- type ErrorHandler
- type KeyProvider
- type Logger
- type MemoryNonceStore
- type MemoryTokenStore
- func (s *MemoryTokenStore) Cleanup(ctx context.Context) error
- func (s *MemoryTokenStore) Close()
- func (s *MemoryTokenStore) Delete(ctx context.Context, tokenID string) error
- func (s *MemoryTokenStore) Get(ctx context.Context, tokenID string) (*TokenRecord, error)
- func (s *MemoryTokenStore) Store(ctx context.Context, record *TokenRecord) (string, error)
- type Metrics
- type MultiKeyProvider
- type NetworkSettings
- type NoOpMetrics
- type NonceStore
- type ObservabilitySettings
- type ObserverHook
- type Option
- func WithClockSkew(duration time.Duration) Option
- func WithErrorHandler(handler ErrorHandler) Option
- func WithJSONErrors() Option
- func WithKeyProvider(provider KeyProvider) Option
- func WithLogger(logger Logger) Option
- func WithMasterKey(key crypto.PublicKey) Option
- func WithMaxRequestSize(size int64) Option
- func WithMetrics(metrics Metrics) Option
- func WithNonceStore(store NonceStore) Option
- func WithObserver(observer ObserverHook) Option
- func WithRequestBuilder(builder RequestBuilder) Option
- func WithRequiredPurposes(purposes ...string) Option
- func WithRevocationChecker(checker revocation.Checker) Option
- func WithSkipPaths(paths ...string) Option
- func WithTokenStore(store TokenStore) Option
- type RequestBuilder
- type SecuritySettings
- type Settings
- type StorageSettings
- type TokenRecord
- type TokenStore
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrMissingProof indicates the Signet-Proof header is missing ErrMissingProof = errors.New("missing Signet-Proof header") // ErrInvalidProof indicates the proof format is invalid ErrInvalidProof = errors.New("invalid proof format") // ErrTokenNotFound indicates the token doesn't exist in the store ErrTokenNotFound = errors.New("token not found") // ErrTokenExpired indicates the token has expired ErrTokenExpired = errors.New("token expired") // ErrTokenNotYetValid indicates the token's NotBefore time hasn't arrived ErrTokenNotYetValid = errors.New("token not yet valid") // ErrTokenRevoked indicates the token has been revoked ErrTokenRevoked = errors.New("token revoked") // ErrClockSkew indicates the request timestamp is outside acceptable bounds ErrClockSkew = errors.New("clock skew detected") // ErrReplayDetected indicates the same nonce was used twice ErrReplayDetected = errors.New("replay attack detected") // ErrInvalidSignature indicates cryptographic verification failed ErrInvalidSignature = errors.New("invalid signature") // ErrKeyNotFound indicates the master key couldn't be retrieved ErrKeyNotFound = errors.New("master key not found") // ErrInternalError indicates an internal server error ErrInternalError = errors.New("internal server error") // ErrPurposeMismatch indicates the token's purpose doesn't match requirements ErrPurposeMismatch = errors.New("token purpose mismatch") // ErrRequestTooLarge indicates the request exceeds maximum allowed size ErrRequestTooLarge = errors.New("request too large") )
Standard authentication errors
Functions ¶
func SignetMiddleware ¶
SignetMiddleware creates HTTP middleware that enforces Signet two-step verification. The middleware validates requests by:
- Verifying the master key signed the ephemeral key
- Verifying the ephemeral key signed the request
- Preventing replay attacks via nonce tracking
- Enforcing time-based validity windows
Returns an error if the middleware configuration is invalid. This allows graceful error handling instead of panicking.
Example usage:
middleware, err := SignetMiddleware(
WithMasterKey(masterPub),
WithClockSkew(30*time.Second),
)
if err != nil {
return fmt.Errorf("failed to create middleware: %w", err)
}
handler := middleware(myHandler)
Types ¶
type AuthContext ¶
type AuthContext struct {
TokenID string
Token *signet.Token
Purpose string
IssuerID string
MasterKeyHash []byte
EphemeralPublicKey crypto.PublicKey
VerifiedAt time.Time
}
AuthContext contains authentication information added to verified requests
func GetAuthContext ¶
func GetAuthContext(r *http.Request) (*AuthContext, bool)
GetAuthContext retrieves the authentication context from a request
type Config ¶
type Config struct {
// Core configuration
ClockSkew time.Duration
TokenStore TokenStore
NonceStore NonceStore
KeyProvider KeyProvider
ErrorHandler ErrorHandler
RequestBuilder RequestBuilder
RevocationChecker revocation.Checker
// Observability
Logger Logger
Metrics Metrics
Observer ObserverHook // Context-based monitoring hook
// Security settings
MaxRequestSize int64 // Maximum request body size (0 = use default 1MB)
// Advanced options
SkipPaths []string
RequiredPurposes []string
}
Config holds middleware configuration
type ErrorHandler ¶
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
ErrorHandler handles authentication errors with custom responses.
var DefaultErrorHandler ErrorHandler = defaultErrorHandler
DefaultErrorHandler is the default error handler for testing/direct use
type KeyProvider ¶
type KeyProvider interface {
// GetMasterKey retrieves the master public key for an issuer.
// Returns ErrKeyNotFound if the key doesn't exist.
GetMasterKey(ctx context.Context, issuerID string) (crypto.PublicKey, error)
// RefreshKeys updates the key cache (optional).
RefreshKeys(ctx context.Context) error
}
KeyProvider defines the interface for retrieving master public keys. This allows for dynamic key management and rotation.
type Logger ¶
type Logger interface {
Debug(msg string, args ...interface{})
Info(msg string, args ...interface{})
Warn(msg string, args ...interface{})
Error(msg string, args ...interface{})
}
Logger defines the logging interface for the middleware.
type MemoryNonceStore ¶
type MemoryNonceStore struct {
// contains filtered or unexported fields
}
MemoryNonceStore implements NonceStore using in-memory storage. Suitable for single-instance deployments and testing.
func NewMemoryNonceStore ¶
func NewMemoryNonceStore() *MemoryNonceStore
NewMemoryNonceStore creates a new in-memory nonce store.
func (*MemoryNonceStore) CheckAndStore ¶
CheckAndStore atomically checks and stores a nonce
func (*MemoryNonceStore) Cleanup ¶
func (s *MemoryNonceStore) Cleanup(ctx context.Context) error
Cleanup removes expired nonces
func (*MemoryNonceStore) Close ¶
func (s *MemoryNonceStore) Close()
Close stops the cleanup goroutine
type MemoryTokenStore ¶
type MemoryTokenStore struct {
// contains filtered or unexported fields
}
MemoryTokenStore implements TokenStore using in-memory storage. Suitable for single-instance deployments and testing.
func NewMemoryTokenStore ¶
func NewMemoryTokenStore() *MemoryTokenStore
NewMemoryTokenStore creates a new in-memory token store.
func (*MemoryTokenStore) Cleanup ¶
func (s *MemoryTokenStore) Cleanup(ctx context.Context) error
Cleanup removes expired tokens
func (*MemoryTokenStore) Close ¶
func (s *MemoryTokenStore) Close()
Close stops the cleanup goroutine
func (*MemoryTokenStore) Delete ¶
func (s *MemoryTokenStore) Delete(ctx context.Context, tokenID string) error
Delete removes a token record
func (*MemoryTokenStore) Get ¶
func (s *MemoryTokenStore) Get(ctx context.Context, tokenID string) (*TokenRecord, error)
Get retrieves a token record by ID
func (*MemoryTokenStore) Store ¶
func (s *MemoryTokenStore) Store(ctx context.Context, record *TokenRecord) (string, error)
Store saves a token record and returns its ID
type Metrics ¶
type Metrics interface {
// RecordAuthResult records the result of an authentication attempt.
RecordAuthResult(result string, duration time.Duration)
// RecordTokenUsage records token usage statistics.
RecordTokenUsage(tokenID string, purpose string)
}
Metrics defines the metrics collection interface.
type MultiKeyProvider ¶
type MultiKeyProvider struct {
// contains filtered or unexported fields
}
MultiKeyProvider implements KeyProvider with support for multiple issuers
func NewMultiKeyProvider ¶
func NewMultiKeyProvider() *MultiKeyProvider
NewMultiKeyProvider creates a key provider supporting multiple issuers
func (*MultiKeyProvider) AddKey ¶
func (p *MultiKeyProvider) AddKey(issuerID string, key crypto.PublicKey)
AddKey registers a master public key for an issuer
func (*MultiKeyProvider) GetMasterKey ¶
func (p *MultiKeyProvider) GetMasterKey(ctx context.Context, issuerID string) (crypto.PublicKey, error)
GetMasterKey retrieves the master public key for an issuer
func (*MultiKeyProvider) RefreshKeys ¶
func (p *MultiKeyProvider) RefreshKeys(ctx context.Context) error
RefreshKeys is a no-op for the in-memory provider
type NetworkSettings ¶
type NetworkSettings struct {
// ChunkedTransferTimeout limits the total time for reading chunked request bodies.
// This prevents DoS attacks via slow-drip chunked transfers that bypass
// Content-Length checks. Set to 0 to disable timeout.
//
// Background (Issue #28 enhancement):
// - Current DoS fix only validates Content-Length header
// - Chunked transfers don't have Content-Length
// - Attacker can send infinite chunks slowly to exhaust connections
// - This timeout closes the connection if chunks take too long
//
// Default: 30 seconds
// Environment variable: SIGNET_CHUNKED_TIMEOUT
//
// Future Implementation (tracked in issue #23):
// - Add io.ReadCloser wrapper with deadline
// - Monitor total read time, not individual chunk time
// - Emit metrics on timeout events
ChunkedTransferTimeout time.Duration `yaml:"chunked_timeout" toml:"chunked_timeout" env:"SIGNET_CHUNKED_TIMEOUT" default:"30s"`
// ReadHeaderTimeout limits time to read request headers.
// Default: 10 seconds
// Environment variable: SIGNET_READ_HEADER_TIMEOUT
ReadHeaderTimeout time.Duration `yaml:"read_header_timeout" toml:"read_header_timeout" env:"SIGNET_READ_HEADER_TIMEOUT" default:"10s"`
// WriteTimeout limits time to write response.
// Default: 30 seconds
// Environment variable: SIGNET_WRITE_TIMEOUT
WriteTimeout time.Duration `yaml:"write_timeout" toml:"write_timeout" env:"SIGNET_WRITE_TIMEOUT" default:"30s"`
// IdleTimeout limits time for keep-alive connections.
// Default: 90 seconds
// Environment variable: SIGNET_IDLE_TIMEOUT
IdleTimeout time.Duration `yaml:"idle_timeout" toml:"idle_timeout" env:"SIGNET_IDLE_TIMEOUT" default:"90s"`
}
NetworkSettings controls network-level behavior.
type NoOpMetrics ¶
type NoOpMetrics = noOpMetrics
NoOpMetrics is an exported no-op metrics implementation for testing
type NonceStore ¶
type NonceStore interface {
// CheckAndStore atomically checks if a nonce exists and stores it if not.
// Returns ErrReplayDetected if the nonce was already used.
// The expiry parameter hints when the nonce can be safely removed.
CheckAndStore(ctx context.Context, nonceKey string, expiry int64) error
// Cleanup removes expired nonces (optional, for maintenance).
Cleanup(ctx context.Context) error
}
NonceStore defines the interface for replay prevention. Each nonce should only be used once within a token's lifetime.
type ObservabilitySettings ¶
type ObservabilitySettings struct {
// MetricsEnabled determines whether to emit Prometheus/StatsD metrics.
// Default: true
// Environment variable: SIGNET_METRICS_ENABLED
MetricsEnabled bool `yaml:"metrics_enabled" toml:"metrics_enabled" env:"SIGNET_METRICS_ENABLED" default:"true"`
// LogLevel controls logging verbosity.
// Options: "debug", "info", "warn", "error"
// Default: "info"
// Environment variable: SIGNET_LOG_LEVEL
LogLevel string `yaml:"log_level" toml:"log_level" env:"SIGNET_LOG_LEVEL" default:"info"`
// TracingEnabled determines whether to emit distributed tracing spans.
// Default: false
// Environment variable: SIGNET_TRACING_ENABLED
TracingEnabled bool `yaml:"tracing_enabled" toml:"tracing_enabled" env:"SIGNET_TRACING_ENABLED" default:"false"`
// TracingProvider specifies the tracing backend.
// Options: "opentelemetry", "jaeger", "zipkin"
// Default: "opentelemetry"
// Environment variable: SIGNET_TRACING_PROVIDER
TracingProvider string `yaml:"tracing_provider" toml:"tracing_provider" env:"SIGNET_TRACING_PROVIDER" default:"opentelemetry"`
}
ObservabilitySettings controls monitoring and logging.
type ObserverHook ¶
type ObserverHook interface {
// OnAuthStart is called before authentication begins.
// Returns a new context (possibly with trace IDs, span IDs, etc.)
OnAuthStart(ctx context.Context, r *http.Request) context.Context
// OnAuthSuccess is called after successful authentication.
// Can emit metrics, log events, close spans, etc.
OnAuthSuccess(ctx context.Context, authCtx *AuthContext)
// OnAuthFailure is called after authentication failure.
// Can emit metrics, log errors, close spans, etc.
OnAuthFailure(ctx context.Context, err error, stage string)
}
ObserverHook defines the interface for observing authentication events. This enables integration with monitoring systems (OpenTelemetry, Prometheus, etc.) via context propagation.
Observer hooks are called at key points in the authentication flow:
- OnAuthStart - Before authentication begins
- OnAuthSuccess - After successful verification
- OnAuthFailure - After verification failure
Context-Based Monitoring Pattern:
- Observers can attach metadata to context (trace IDs, span IDs)
- Downstream services can read this metadata for distributed tracing
- No tight coupling to specific monitoring tools
Example: OpenTelemetry Integration
type OTelObserver struct{ tracer trace.Tracer }
func (o *OTelObserver) OnAuthStart(ctx context.Context, r *http.Request) context.Context {
ctx, span := o.tracer.Start(ctx, "signet.authenticate")
return ctx
}
func (o *OTelObserver) OnAuthSuccess(ctx context.Context, authCtx *AuthContext) {
span := trace.SpanFromContext(ctx)
span.SetAttributes(attribute.String("token_id", authCtx.TokenID))
span.End()
}
type Option ¶
type Option func(*Config)
Option configures the Signet middleware.
func WithClockSkew ¶
WithClockSkew sets the maximum allowed time difference between client and server. Default is 30 seconds.
func WithErrorHandler ¶
func WithErrorHandler(handler ErrorHandler) Option
WithErrorHandler sets a custom error response handler.
func WithKeyProvider ¶
func WithKeyProvider(provider KeyProvider) Option
WithKeyProvider sets a custom key provider for retrieving master keys. This enables dynamic key management and rotation.
func WithLogger ¶
WithLogger sets a custom logger for the middleware.
func WithMasterKey ¶
WithMasterKey sets a static master public key for verification. This is a convenience method for simple deployments. Accepts any crypto.PublicKey (Ed25519, ML-DSA, etc.).
func WithMaxRequestSize ¶
WithMaxRequestSize sets the maximum allowed request body size in bytes. This protects against DoS attacks via oversized requests. Default: 1MB (1048576 bytes) if not configured.
Example:
middleware := SignetMiddleware(
WithMaxRequestSize(5 * 1024 * 1024), // 5MB limit
)
func WithNonceStore ¶
func WithNonceStore(store NonceStore) Option
WithNonceStore sets a custom nonce storage backend for replay prevention. Default is in-memory storage.
func WithObserver ¶
func WithObserver(observer ObserverHook) Option
WithObserver configures a custom observer hook for monitoring. Observer hooks enable integration with distributed tracing systems (OpenTelemetry, Jaeger) and custom monitoring solutions via context propagation.
Example: OpenTelemetry Integration
type OTelObserver struct{ tracer trace.Tracer }
func (o *OTelObserver) OnAuthStart(ctx context.Context, r *http.Request) context.Context {
ctx, span := o.tracer.Start(ctx, "signet.authenticate")
return ctx
}
middleware := SignetMiddleware(
WithObserver(&OTelObserver{tracer: tracer}),
)
func WithRequestBuilder ¶
func WithRequestBuilder(builder RequestBuilder) Option
WithRequestBuilder sets a custom canonical request builder.
func WithRequiredPurposes ¶
WithRequiredPurposes enforces that tokens must have one of the specified purposes. This provides additional access control beyond cryptographic verification.
func WithRevocationChecker ¶
func WithRevocationChecker(checker revocation.Checker) Option
WithRevocationChecker sets a custom revocation checker for the middleware.
func WithSkipPaths ¶
WithSkipPaths configures paths that bypass authentication. Useful for health checks and public endpoints.
func WithTokenStore ¶
func WithTokenStore(store TokenStore) Option
WithTokenStore sets a custom token storage backend. Default is in-memory storage.
type RequestBuilder ¶
type RequestBuilder interface {
// Build creates the canonical request bytes that were signed.
Build(r *http.Request, proof *header.SignetProof) ([]byte, error)
}
RequestBuilder constructs the canonical request representation for signing. Different applications may need different canonicalization strategies.
var DefaultRequestBuilder RequestBuilder = defaultRequestBuilder
DefaultRequestBuilder is the default request builder for testing/direct use
type SecuritySettings ¶
type SecuritySettings struct {
// MaxRequestSize limits request body size to prevent DoS attacks.
// Default: 1MB (1048576 bytes)
// Environment variable: SIGNET_MAX_REQUEST_SIZE
MaxRequestSize int64 `yaml:"max_request_size" toml:"max_request_size" env:"SIGNET_MAX_REQUEST_SIZE" default:"1048576"`
// ClockSkew is the maximum allowed time difference between client and server.
// Default: 30 seconds
// Environment variable: SIGNET_CLOCK_SKEW
ClockSkew time.Duration `yaml:"clock_skew" toml:"clock_skew" env:"SIGNET_CLOCK_SKEW" default:"30s"`
// MaxTokensPerUser limits tokens per user to prevent resource exhaustion.
// Default: 100
// Environment variable: SIGNET_MAX_TOKENS_PER_USER
MaxTokensPerUser int `yaml:"max_tokens_per_user" toml:"max_tokens_per_user" env:"SIGNET_MAX_TOKENS_PER_USER" default:"100"`
}
SecuritySettings controls security-related middleware behavior.
type Settings ¶
type Settings struct {
// Security settings
Security SecuritySettings
// Storage settings
Storage StorageSettings
// Observability settings
Observability ObservabilitySettings
// Network settings
Network NetworkSettings
}
Settings represents runtime configuration for Signet middleware. This struct is designed to be loaded from YAML/TOML configuration files and environment variables. See issue #23 for full configuration system design.
Future Configuration System Design:
- Load from YAML/TOML files (e.g., /etc/signet/config.yaml)
- Override with environment variables (e.g., SIGNET_MAX_REQUEST_SIZE)
- Validate on startup with clear error messages
- Support hot reload for safe settings (log levels, timeouts)
Example YAML configuration:
security: max_request_size: 1048576 # 1MB clock_skew: 30s chunked_timeout: 30s storage: token_store: redis redis_url: redis://localhost:6379 observability: metrics_enabled: true log_level: info tracing_enabled: true
func DefaultSettings ¶
func DefaultSettings() *Settings
DefaultSettings returns a Settings instance with sensible defaults. This is the baseline configuration that can be overridden via files/env vars.
func (*Settings) ApplyToConfig ¶
ApplyToConfig applies Settings to the middleware Config struct. This bridges the gap between file-based Settings and runtime Config.
Future Implementation (issue #23):
- Map Settings fields to Config fields
- Instantiate storage backends based on Settings
- Configure logger based on LogLevel
- Set up metrics/tracing exporters
Example usage:
settings := LoadSettings("config.yaml")
config := &Config{...}
settings.ApplyToConfig(config)
func (*Settings) Validate ¶
Validate checks if the settings are valid. Returns an error if any setting is invalid or incompatible.
Future Implementation (issue #23):
- Validate MaxRequestSize > 0 and < 100MB
- Validate ClockSkew >= 0 and <= 5 minutes
- Validate storage backends are available
- Check Redis/Postgres URLs are parseable
- Validate log levels are recognized
- Ensure timeout values are reasonable
type StorageSettings ¶
type StorageSettings struct {
// TokenStoreType specifies the token storage backend.
// Options: "memory", "redis", "postgres", "dynamodb"
// Default: "memory"
// Environment variable: SIGNET_TOKEN_STORE
TokenStoreType string `yaml:"token_store" toml:"token_store" env:"SIGNET_TOKEN_STORE" default:"memory"`
// NonceStoreType specifies the nonce storage backend.
// Options: "memory", "redis"
// Default: "memory"
// Environment variable: SIGNET_NONCE_STORE
NonceStoreType string `yaml:"nonce_store" toml:"nonce_store" env:"SIGNET_NONCE_STORE" default:"memory"`
// CleanupInterval determines how often expired tokens/nonces are removed.
// Default: 5 minutes
// Environment variable: SIGNET_CLEANUP_INTERVAL
CleanupInterval time.Duration `yaml:"cleanup_interval" toml:"cleanup_interval" env:"SIGNET_CLEANUP_INTERVAL" default:"5m"`
// RedisURL is the connection string for Redis storage backends.
// Example: "redis://localhost:6379/0"
// Environment variable: SIGNET_REDIS_URL
RedisURL string `yaml:"redis_url" toml:"redis_url" env:"SIGNET_REDIS_URL"`
// PostgresURL is the connection string for PostgreSQL storage.
// Example: "postgres://user:pass@localhost/signet?sslmode=require"
// Environment variable: SIGNET_POSTGRES_URL
PostgresURL string `yaml:"postgres_url" toml:"postgres_url" env:"SIGNET_POSTGRES_URL"`
}
StorageSettings controls token and nonce storage backends.
type TokenRecord ¶
type TokenRecord struct {
Token *signet.Token
MasterPublicKey crypto.PublicKey
EphemeralPublicKey crypto.PublicKey
BindingSignature []byte
IssuedAt time.Time
Purpose string
Metadata map[string]string // Optional metadata for extensions
}
TokenRecord represents a stored token with its cryptographic context. This contains all information needed for two-step verification.
type TokenStore ¶
type TokenStore interface {
// Get retrieves a token record by its ID.
// The tokenID parameter must be a 32-character hex string from hex.EncodeToString(JTI).
// Returns ErrTokenNotFound if the token doesn't exist.
Get(ctx context.Context, tokenID string) (*TokenRecord, error)
// Store saves a token record and returns its ID.
// The returned tokenID will be a 32-character hex string from hex.EncodeToString(token.JTI).
Store(ctx context.Context, record *TokenRecord) (string, error)
// Delete removes a token record (optional, for revocation).
Delete(ctx context.Context, tokenID string) error
// Cleanup removes expired tokens (optional, for maintenance).
// Implementations may handle this automatically.
Cleanup(ctx context.Context) error
}
TokenStore defines the interface for token storage and retrieval. Implementations can use in-memory storage, Redis, databases, or any other backend.
Token ID Format:
- Token IDs are derived from the token's JTI (CBOR field 4)
- Format: hex.EncodeToString(JTI) = 32 hex characters (from 16 bytes)
- Example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
Security Considerations:
- MUST use full 16-byte JTI to prevent collisions (birthday paradox)
- Truncating to 8 bytes hits 50% collision probability at ~4 billion tokens
- Full JTI provides 2^64 uniqueness (computationally infeasible to collide)
Migration Notes:
- Previous versions used 8-byte truncation (16 hex chars)
- Alpha software uses big bang migration (all old tokens invalidated)
- Production systems should implement dual lookup during transition