Documentation
¶
Overview ¶
Package security provides security features for OAuth including encryption, rate limiting, audit logging, and secure header management.
Package security provides security-related functionality for the OAuth server, including rate limiting, encryption, IP validation, and audit logging.
Rate Limiting ¶
The RateLimiter provides per-identifier rate limiting using a token bucket algorithm with automatic memory management through LRU (Least Recently Used) eviction.
## Memory Management
To prevent unbounded memory growth under distributed attacks, the rate limiter implements a configurable maximum entries limit. When this limit is reached, the least recently used entries are automatically evicted.
Default configuration:
- MaxEntries: 10,000 unique identifiers
- CleanupInterval: 5 minutes
- IdleTimeout: 30 minutes
## Example Usage
// Create rate limiter with default settings (10,000 max entries)
limiter := security.NewRateLimiter(10, 20, logger)
defer limiter.Stop()
// Create rate limiter with custom max entries
limiter := security.NewRateLimiterWithConfig(10, 20, 5000, logger)
defer limiter.Stop()
// Check if request is allowed
if !limiter.Allow(clientIP) {
// Rate limit exceeded
return http.StatusTooManyRequests
}
// Monitor memory usage
stats := limiter.GetStats()
if stats.MemoryPressure > 80.0 {
logger.Warn("Rate limiter memory pressure high",
"pressure", stats.MemoryPressure,
"current_entries", stats.CurrentEntries,
"max_entries", stats.MaxEntries)
}
## Monitoring and Alerting
The GetStats() method provides metrics for monitoring:
- CurrentEntries: Number of tracked identifiers
- MaxEntries: Configured limit (0 = unlimited)
- TotalEvictions: Number of LRU evictions performed
- TotalCleanups: Number of cleanup operations completed
- MemoryPressure: Percentage of max capacity used (0-100)
Set up alerts when:
- MemoryPressure consistently > 80%: Consider increasing MaxEntries
- TotalEvictions increasing rapidly: Possible distributed attack
- CurrentEntries near MaxEntries: May need capacity adjustment
## Security Considerations
The rate limiter is designed to prevent:
- Memory exhaustion from distributed attacks
- Resource exhaustion through controlled limits
- Timing attacks (constant-time operations where possible)
The LRU eviction strategy ensures that legitimate users (who make repeated requests) are less likely to be evicted, while one-time attack IPs are evicted first.
Index ¶
- Constants
- func GenerateKey() ([]byte, error)
- func GenerateRequestID() string
- func GetClientIP(r *http.Request, trustProxy bool, trustedProxyCount int) string
- func GetRequestID(ctx context.Context) string
- func IsTokenExpired(expiresAt time.Time) bool
- func IsTokenExpiredWithGracePeriod(expiresAt time.Time, gracePeriod time.Duration) bool
- func IsTokenExpiringSoon(expiresAt time.Time, threshold time.Duration) bool
- func KeyFromBase64(s string) ([]byte, error)
- func KeyToBase64(key []byte) string
- func RequestIDMiddleware(next http.Handler) http.Handler
- func SetInterstitialSecurityHeaders(w http.ResponseWriter, serverURL string)
- func SetSecurityHeaders(w http.ResponseWriter, serverURL string)
- func WithRequestID(ctx context.Context, requestID string) context.Context
- type Auditor
- func (a *Auditor) LogAuthFailure(userID, clientID, ipAddress, reason string)
- func (a *Auditor) LogClientRegistered(clientID, clientType, ipAddress string)
- func (a *Auditor) LogClientRegistrationRateLimitExceeded(ipAddress string)
- func (a *Auditor) LogEvent(event Event)
- func (a *Auditor) LogInvalidPKCE(clientID, ipAddress, reason string)
- func (a *Auditor) LogInvalidRedirect(clientID, ipAddress, uri, reason string)
- func (a *Auditor) LogRateLimitExceeded(ipAddress, userID string)
- func (a *Auditor) LogSuspiciousActivity(userID, clientID, ipAddress, description string)
- func (a *Auditor) LogTokenIssued(userID, clientID, ipAddress, scope string)
- func (a *Auditor) LogTokenRefreshed(userID, clientID, ipAddress string, rotated bool)
- func (a *Auditor) LogTokenReuse(userID, ipAddress string)
- func (a *Auditor) LogTokenRevoked(userID, clientID, ipAddress, tokenType string)
- type ClientRegistrationRateLimiter
- type Encryptor
- type Event
- type RateLimiter
- func NewRateLimiter(requestsPerSecond, burst int, logger *slog.Logger) *RateLimiter
- func NewRateLimiterWithConfig(requestsPerSecond, burst, maxEntries int, logger *slog.Logger) *RateLimiter
- func NewRateLimiterWithFullConfig(requestsPerSecond, burst, maxEntries int, cleanupInterval time.Duration, ...) *RateLimiter
- type RegistrationStats
- type Stats
Constants ¶
const ( // DefaultMaxRegistrationsPerHour is the default limit for client registrations per IP per hour DefaultMaxRegistrationsPerHour = 10 // DefaultRegistrationWindow is the default time window for rate limiting (1 hour) DefaultRegistrationWindow = time.Hour // DefaultRegistrationCleanupInterval is how often the cleanup goroutine runs DefaultRegistrationCleanupInterval = 15 * time.Minute // DefaultMaxRegistrationEntries is the maximum number of IPs to track DefaultMaxRegistrationEntries = 10000 )
const ( // EventTokenIssued is logged when a new access token is issued to a client EventTokenIssued = "token_issued" // EventTokenRefreshed is logged when an access token is refreshed using a refresh token EventTokenRefreshed = "token_refreshed" // EventTokenProactivelyRefreshed is logged when a token is proactively refreshed before expiry EventTokenProactivelyRefreshed = "token_proactively_refreshed" // EventTokenRevoked is logged when a token is revoked by the user or client EventTokenRevoked = "token_revoked" // EventAllTokensRevoked is logged when all tokens for a user are revoked EventAllTokensRevoked = "all_tokens_revoked" //nolint:gosec // G101: False positive - this is an event type name, not a credential // EventAuthorizationFlowStarted is logged when an authorization flow is initiated EventAuthorizationFlowStarted = "authorization_flow_started" // EventAuthorizationCodeIssued is logged when an authorization code is issued EventAuthorizationCodeIssued = "authorization_code_issued" // EventAuthorizationCodeReuseDetected is logged when an authorization code is reused (attack) EventAuthorizationCodeReuseDetected = "authorization_code_reuse_detected" // EventClientRegistered is logged when a new OAuth client is registered EventClientRegistered = "client_registered" // EventClientRegisteredViaTrustedScheme is logged when a client is registered without a token // because it uses only trusted custom URI schemes (e.g., cursor://, vscode://). // This enables compatibility with MCP clients that don't support registration tokens. EventClientRegisteredViaTrustedScheme = "client_registered_via_trusted_scheme" // EventClientRegistrationRejected is logged when client registration is rejected for security reasons EventClientRegistrationRejected = "client_registration_rejected" // EventClientRegistrationRateLimitExceeded is logged when client registration rate limit is exceeded EventClientRegistrationRateLimitExceeded = "client_registration_rate_limit_exceeded" // EventAuthFailure is logged when authentication fails (wrong credentials, etc.) EventAuthFailure = "auth_failure" // EventRateLimitExceeded is logged when a rate limit is exceeded EventRateLimitExceeded = "rate_limit_exceeded" // EventInvalidPKCE is logged when PKCE validation fails EventInvalidPKCE = "invalid_pkce" // EventPKCEValidationFailed is logged when PKCE code_verifier validation fails EventPKCEValidationFailed = "pkce_validation_failed" // EventPKCERequiredForPublicClient is logged when a public client attempts flow without PKCE EventPKCERequiredForPublicClient = "pkce_required_for_public_client" // EventInsecurePublicClientWithoutPKCE is logged when insecure flow is attempted EventInsecurePublicClientWithoutPKCE = "insecure_public_client_without_pkce" // EventTokenReuseDetected is logged when refresh token reuse is detected (theft) EventTokenReuseDetected = "token_reuse_detected" //nolint:gosec // G101: False positive - this is an event type name, not a credential // EventRefreshTokenReuseDetected is logged when a refresh token is reused in the same family EventRefreshTokenReuseDetected = "refresh_token_reuse_detected" // EventRefreshTokenMissingClientBinding is logged when a refresh token lacks client binding // This may occur for legacy tokens issued before OAuth 2.1 client binding was implemented EventRefreshTokenMissingClientBinding = "refresh_token_missing_client_binding" // EventRefreshTokenClientBindingMismatch is logged when the requesting client doesn't match // the client that was originally issued the refresh token (OAuth 2.1 Section 6 violation) // This is a critical security event indicating possible cross-client token theft EventRefreshTokenClientBindingMismatch = "refresh_token_client_binding_mismatch" // EventRefreshTokenFamilyRevoked is logged when an entire refresh token family is revoked // during explicit token revocation (all tokens sharing the same family ID become invalid) EventRefreshTokenFamilyRevoked = "refresh_token_family_revoked" // EventRevokedTokenFamilyReuseAttempt is logged when a revoked token family is accessed EventRevokedTokenFamilyReuseAttempt = "revoked_token_family_reuse_attempt" // EventSuspiciousActivity is logged for general suspicious behavior EventSuspiciousActivity = "suspicious_activity" // EventInvalidRedirect is logged when an invalid redirect URI is used EventInvalidRedirect = "invalid_redirect" // EventScopeEscalationAttempt is logged when a client tries to escalate scopes EventScopeEscalationAttempt = "scope_escalation_attempt" // EventScopeDefaultsApplied is logged when provider default scopes are used (forensics/compliance) EventScopeDefaultsApplied = "scope_defaults_applied" // EventResourceMismatch is logged when resource parameter doesn't match (RFC 8707) EventResourceMismatch = "resource_mismatch" // EventCrossClientTokenAccepted is logged when a token is accepted via TrustedAudiences. // This occurs in SSO scenarios where tokens issued to a trusted upstream (e.g., muster) // are accepted by a downstream MCP server. This event is logged for security monitoring // and forensics to track cross-client token usage patterns. EventCrossClientTokenAccepted = "cross_client_token_accepted" // EventForwardedIDTokenAccepted is logged when a forwarded ID token (JWT) is validated // and accepted via JWKS signature verification. This is part of SSO token forwarding // where an upstream MCP server's ID token is passed as a Bearer token to downstream services. EventForwardedIDTokenAccepted = "forwarded_id_token_accepted" // EventInvalidProviderCallback is logged when provider callback validation fails EventInvalidProviderCallback = "invalid_provider_callback" // EventProviderStateMismatch is logged when provider state parameter doesn't match EventProviderStateMismatch = "provider_state_mismatch" // EventProviderCodeExchangeFailed is logged when code exchange with provider fails (PKCE, etc.) EventProviderCodeExchangeFailed = "provider_code_exchange_failed" // EventProviderRevocationThresholdExceeded is logged when provider revocation partial failure occurs EventProviderRevocationThresholdExceeded = "provider_revocation_threshold_exceeded" // EventProviderRevocationCompleteFailure is logged when all provider revocation attempts fail EventProviderRevocationCompleteFailure = "provider_revocation_complete_failure" // EventTokenRevocationNotSupported is logged when provider doesn't support token revocation EventTokenRevocationNotSupported = "token_revocation_not_supported" // EventProactiveRefreshFailed is logged when proactive token refresh fails EventProactiveRefreshFailed = "proactive_refresh_failed" )
Event type constants for security audit logging. These constants ensure consistency across the codebase and prevent typos when logging security-relevant events.
const ( // DefaultMaxEntries is the default maximum number of tracked identifiers DefaultMaxEntries = 10000 // DefaultCleanupInterval is how often the cleanup goroutine runs DefaultCleanupInterval = 5 * time.Minute // DefaultIdleTimeout is how long an entry can be idle before cleanup DefaultIdleTimeout = 30 * time.Minute )
const ( // DefaultClockSkewGracePeriod is the default grace period for token expiration checks // This prevents false expiration errors due to time synchronization issues // between different systems (client, server, provider). // // Security Rationale: // - Prevents false expiration errors due to minor time differences // - Balances security (minimize token lifetime extension) with usability // - 5 seconds is a conservative value that handles typical NTP drift // // Trade-offs: // - Allows tokens to be used up to 5 seconds beyond their true expiration // - This is acceptable for most use cases and improves reliability // - For high-security scenarios, this can be reduced or disabled DefaultClockSkewGracePeriod = 5 * time.Second )
const InterstitialScriptHash = "sha256-BSPDdcxaKPs2IRkTMWvH7KxMRr/MuFv1HaDJlxd1UTI="
InterstitialScriptHash is the SHA-256 hash of the static inline script used in the success interstitial page. This hash is computed from the minified script content and allows the script to execute under a strict Content-Security-Policy.
The script reads the redirect URL from the button's href attribute (which is set by the template), so the script content is static and the hash is stable.
To regenerate this hash if the script changes:
echo -n '<script content>' | openssl dgst -sha256 -binary | base64
const RequestIDHeader = "X-Request-ID"
RequestIDHeader is the HTTP header for request IDs
Variables ¶
This section is empty.
Functions ¶
func GenerateKey ¶
GenerateKey generates a new 32-byte encryption key for AES-256
func GenerateRequestID ¶ added in v0.1.24
func GenerateRequestID() string
GenerateRequestID generates a cryptographically secure random request ID. It uses crypto/rand to generate 16 bytes (128 bits) of entropy and encodes them as a 22-character base64url string without padding.
Request IDs are used for audit trails, security correlation, and debugging. The function panics if the system's random number generator fails, which indicates a critical system-level security failure.
func GetClientIP ¶
GetClientIP extracts the real client IP address from the request Supports X-Forwarded-For and X-Real-IP headers when behind a proxy
SECURITY CONSIDERATIONS: - Only enable trustProxy when behind a trusted reverse proxy (nginx, haproxy, etc.) - X-Forwarded-For format: "client, proxy1, proxy2, ..." - trustedProxyCount specifies how many proxies to trust from the right - This prevents X-Forwarded-For spoofing in multi-proxy setups
func GetRequestID ¶ added in v0.1.24
GetRequestID retrieves the request ID from the context
func IsTokenExpired ¶
IsTokenExpired checks if a token is expired with default clock skew grace period
func IsTokenExpiredWithGracePeriod ¶ added in v0.1.1
IsTokenExpiredWithGracePeriod checks if a token is expired with custom clock skew grace period
func IsTokenExpiringSoon ¶
IsTokenExpiringSoon checks if a token will expire within the given threshold
func KeyFromBase64 ¶
KeyFromBase64 decodes a base64-encoded encryption key
func KeyToBase64 ¶
KeyToBase64 encodes an encryption key to base64
func RequestIDMiddleware ¶ added in v0.1.24
RequestIDMiddleware is HTTP middleware that generates and propagates request IDs.
Security behavior:
- Preserves valid request IDs from upstream proxies for audit trail continuity
- Validates upstream IDs to prevent header injection attacks (CRLF, DoS)
- Generates new cryptographically secure ID if upstream ID is missing or invalid
- Adds request ID to response headers for end-to-end correlation
func SetInterstitialSecurityHeaders ¶ added in v0.1.48
func SetInterstitialSecurityHeaders(w http.ResponseWriter, serverURL string)
SetInterstitialSecurityHeaders sets security headers for the OAuth success interstitial page. This is similar to SetSecurityHeaders but includes a hash-based CSP exception for the inline redirect script.
Security considerations:
- Uses hash-based script allowlisting (CSP Level 2) instead of 'unsafe-inline'
- The script hash is computed from a static script that reads the redirect URL from the DOM, ensuring the hash remains stable across different redirect URLs
- style-src 'unsafe-inline' is required because the CSS contains dynamic template variables (colors, gradients, custom CSS) that change per-request, making hash-based CSP impossible for styles. This is acceptable because CSS cannot execute arbitrary code - the risk is significantly lower than for scripts.
- img-src restricts images to HTTPS sources only (plus data: for inline SVG icons)
func SetSecurityHeaders ¶
func SetSecurityHeaders(w http.ResponseWriter, serverURL string)
SetSecurityHeaders sets comprehensive security headers on HTTP responses These headers protect against various web vulnerabilities
Types ¶
type Auditor ¶
type Auditor struct {
// contains filtered or unexported fields
}
Auditor handles security event logging with PII protection.
func NewAuditor ¶
NewAuditor creates a new security auditor
func (*Auditor) LogAuthFailure ¶
LogAuthFailure logs an authentication failure
func (*Auditor) LogClientRegistered ¶
LogClientRegistered logs when a new client is registered
func (*Auditor) LogClientRegistrationRateLimitExceeded ¶ added in v0.1.20
LogClientRegistrationRateLimitExceeded logs when client registration rate limit is exceeded
func (*Auditor) LogInvalidPKCE ¶ added in v0.1.3
LogInvalidPKCE logs when PKCE validation fails
func (*Auditor) LogInvalidRedirect ¶ added in v0.1.3
LogInvalidRedirect logs invalid redirect URI attempts
func (*Auditor) LogRateLimitExceeded ¶
LogRateLimitExceeded logs a rate limit violation
func (*Auditor) LogSuspiciousActivity ¶ added in v0.1.3
LogSuspiciousActivity logs suspicious activity
func (*Auditor) LogTokenIssued ¶
LogTokenIssued logs when a token is issued
func (*Auditor) LogTokenRefreshed ¶
LogTokenRefreshed logs when a token is refreshed
func (*Auditor) LogTokenReuse ¶ added in v0.1.3
LogTokenReuse logs when refresh token reuse is detected (security event)
func (*Auditor) LogTokenRevoked ¶
LogTokenRevoked logs when a token is revoked
type ClientRegistrationRateLimiter ¶ added in v0.1.20
type ClientRegistrationRateLimiter struct {
// contains filtered or unexported fields
}
ClientRegistrationRateLimiter provides time-windowed rate limiting for client registrations to prevent resource exhaustion through repeated registration/deletion cycles.
Lifecycle Management:
The rate limiter starts a background cleanup goroutine when created. Always call Stop() when shutting down to prevent goroutine leaks:
rl := security.NewClientRegistrationRateLimiter(logger) defer rl.Stop() // Critical: prevents goroutine leak
The Stop() method is safe to call multiple times and can be called concurrently.
func NewClientRegistrationRateLimiter ¶ added in v0.1.20
func NewClientRegistrationRateLimiter(logger *slog.Logger) *ClientRegistrationRateLimiter
NewClientRegistrationRateLimiter creates a new client registration rate limiter with default settings.
The rate limiter starts a background cleanup goroutine. Always call Stop() when done:
rl := security.NewClientRegistrationRateLimiter(logger) defer rl.Stop() // Important: cleanup background goroutine
func NewClientRegistrationRateLimiterWithConfig ¶ added in v0.1.20
func NewClientRegistrationRateLimiterWithConfig(maxPerWindow int, window time.Duration, maxEntries int, logger *slog.Logger) *ClientRegistrationRateLimiter
NewClientRegistrationRateLimiterWithConfig creates a new client registration rate limiter with custom configuration.
The rate limiter starts a background cleanup goroutine. Always call Stop() when done:
rl := security.NewClientRegistrationRateLimiterWithConfig(10, time.Hour, 10000, logger) defer rl.Stop() // Important: cleanup background goroutine
func (*ClientRegistrationRateLimiter) Allow ¶ added in v0.1.20
func (rl *ClientRegistrationRateLimiter) Allow(ip string) bool
Allow checks if a client registration from the given IP is allowed Returns true if allowed, false if rate limit exceeded
func (*ClientRegistrationRateLimiter) Cleanup ¶ added in v0.1.20
func (rl *ClientRegistrationRateLimiter) Cleanup()
Cleanup removes entries that haven't been accessed recently Entries are considered inactive if their last access is older than 2x the window
func (*ClientRegistrationRateLimiter) GetStats ¶ added in v0.1.20
func (rl *ClientRegistrationRateLimiter) GetStats() RegistrationStats
GetStats returns current rate limiter statistics for monitoring and alerting
func (*ClientRegistrationRateLimiter) Stop ¶ added in v0.1.20
func (rl *ClientRegistrationRateLimiter) Stop()
Stop gracefully stops the cleanup goroutine and releases resources.
This method MUST be called when shutting down to prevent goroutine leaks. It is safe to call multiple times and can be called concurrently from multiple goroutines (uses sync.Once internally).
Best practice: Use defer immediately after creating the rate limiter:
rl := security.NewClientRegistrationRateLimiter(logger) defer rl.Stop()
type Encryptor ¶
type Encryptor struct {
// contains filtered or unexported fields
}
Encryptor handles token encryption at rest using AES-256-GCM.
func NewEncryptor ¶
NewEncryptor creates a new encryptor. If key is nil or empty, encryption is disabled. The key must be exactly 32 bytes for AES-256.
type Event ¶
type Event struct {
Type string
UserID string
ClientID string
IPAddress string
UserAgent string // User-Agent header for detecting automated attacks
RequestID string // Unique request ID for correlating log entries
Details map[string]any
Timestamp time.Time
}
Event represents a security audit event
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter provides per-identifier rate limiting using token bucket algorithm with LRU eviction to prevent unbounded memory growth.
func NewRateLimiter ¶
func NewRateLimiter(requestsPerSecond, burst int, logger *slog.Logger) *RateLimiter
NewRateLimiter creates a new rate limiter with automatic cleanup and LRU eviction. Default max entries is 10,000. Use NewRateLimiterWithConfig for custom max entries.
func NewRateLimiterWithConfig ¶ added in v0.1.19
func NewRateLimiterWithConfig(requestsPerSecond, burst, maxEntries int, logger *slog.Logger) *RateLimiter
NewRateLimiterWithConfig creates a new rate limiter with custom max entries configuration. maxEntries controls the maximum number of unique identifiers tracked simultaneously. When limit is reached, least recently used entries are evicted. Set maxEntries to 0 for unlimited (not recommended for production).
func NewRateLimiterWithFullConfig ¶ added in v0.1.24
func NewRateLimiterWithFullConfig(requestsPerSecond, burst, maxEntries int, cleanupInterval time.Duration, logger *slog.Logger) *RateLimiter
NewRateLimiterWithFullConfig creates a new rate limiter with custom configuration including cleanup interval. maxEntries controls the maximum number of unique identifiers tracked simultaneously. cleanupInterval controls how often idle entries are cleaned up. When maxEntries limit is reached, least recently used entries are evicted. Set maxEntries to 0 for unlimited (not recommended for production). Set cleanupInterval to 0 to use default (5 minutes).
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(identifier string) bool
Allow checks if a request from the given identifier is allowed. Implements LRU eviction when max entries limit is reached.
func (*RateLimiter) Cleanup ¶
func (rl *RateLimiter) Cleanup(maxIdleTime time.Duration)
Cleanup removes inactive limiters that haven't been accessed for the given duration. Also removes corresponding entries from the LRU list.
func (*RateLimiter) GetStats ¶ added in v0.1.19
func (rl *RateLimiter) GetStats() Stats
GetStats returns current rate limiter statistics for monitoring and alerting. This is useful for detecting memory pressure and tuning maxEntries configuration.
func (*RateLimiter) Stop ¶ added in v0.1.1
func (rl *RateLimiter) Stop()
Stop gracefully stops the cleanup goroutine. Safe to call multiple times concurrently. Uses sync.Once to guarantee exactly-once execution and prevent race conditions.
type RegistrationStats ¶ added in v0.1.20
type RegistrationStats struct {
CurrentEntries int // Current number of tracked IPs
MaxEntries int // Maximum allowed entries (0 = unlimited)
TotalBlocked int64 // Total registrations blocked
TotalAllowed int64 // Total registrations allowed
TotalEvictions int64 // Total number of LRU evictions
TotalCleanups int64 // Total number of cleanup operations
MaxPerWindow int // Maximum registrations per window
Window string // Time window duration
MemoryPressure float64 // Percentage of max capacity used (0-100)
}
RegistrationStats holds client registration rate limiter statistics for monitoring
type Stats ¶ added in v0.1.19
type Stats struct {
CurrentEntries int // Current number of tracked identifiers
MaxEntries int // Maximum allowed entries (0 = unlimited)
TotalEvictions int64 // Total number of LRU evictions
TotalCleanups int64 // Total number of cleanup operations
MemoryPressure float64 // Percentage of max capacity used (0-100)
}
Stats holds rate limiter statistics for monitoring