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 DecodeKey(s string) ([]byte, error)
- 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 IsTrustedProxy(remoteAddr string, cidrs []*net.IPNet) bool
- func KeyFromBase64(s string) ([]byte, error)
- func KeyFromHex(s string) ([]byte, error)
- func KeyToBase64(key []byte) string
- func KeyToHex(key []byte) string
- func RateLimitBucket(ip string) string
- func RequestIDMiddleware(next http.Handler) http.Handler
- func SetAuditDropRecorder(fn auditDropRecorder)
- func SetDiscoveryCacheHeaders(w http.ResponseWriter, maxAge time.Duration)
- func SetEncryptionMetricRecorder(fn encryptionMetricRecorder)
- func SetInterstitialSecurityHeaders(w http.ResponseWriter, serverURL string)
- func SetSecurityHeaders(w http.ResponseWriter, serverURL string)
- func ValidateKeyEntropy(key []byte) error
- func WithRequestID(ctx context.Context, requestID string) context.Context
- type Auditor
- func (a *Auditor) LogAuthFailure(ctx context.Context, userID, clientID, ipAddress, reason string)
- func (a *Auditor) LogClientRegistered(ctx context.Context, clientID, clientType, ipAddress string)
- func (a *Auditor) LogClientRegistrationRateLimitExceeded(ctx context.Context, ipAddress string)
- func (a *Auditor) LogEvent(ctx context.Context, event Event)
- func (a *Auditor) LogInvalidPKCE(ctx context.Context, clientID, ipAddress, reason string)
- func (a *Auditor) LogInvalidRedirect(ctx context.Context, clientID, ipAddress, uri, reason string)
- func (a *Auditor) LogRateLimitExceeded(ctx context.Context, ipAddress, userID string)
- func (a *Auditor) LogSuspiciousActivity(ctx context.Context, userID, clientID, ipAddress, description string)
- func (a *Auditor) LogTokenIssued(ctx context.Context, userID, clientID, ipAddress, scope string)
- func (a *Auditor) LogTokenRefreshed(ctx context.Context, userID, clientID, ipAddress string, rotated bool)
- func (a *Auditor) LogTokenReuse(ctx context.Context, userID, ipAddress string)
- func (a *Auditor) LogTokenRevoked(ctx context.Context, userID, clientID, ipAddress, tokenType string)
- type AuditorOption
- type ClientRegistrationRateLimiter
- type Encryptor
- type Event
- type KeyRing
- 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" // EventClientRegisteredViaTrustedRedirectURI is logged when a client is registered without a token // because every redirect_uri matches the TrustedPublicRegistrationRedirectURIs allowlist. EventClientRegisteredViaTrustedRedirectURI = "client_registered_via_trusted_redirect_uri" // 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" // EventSelfIssuedJWTAccepted is logged when a self-issued JWT access token // (server.AccessTokenFormatJWT mode, RFC 9068) is validated locally via // signature verification against the configured public key. Useful for // audit trails and for distinguishing local-validation hot-path bearers // from opaque-storage bearers in dashboards. EventSelfIssuedJWTAccepted = "self_issued_jwt_accepted" // EventSelfIssuedJWTRevoked is logged when /oauth/revoke records a // self-issued JWT access token's jti in the RevokedTokenStore denylist. EventSelfIssuedJWTRevoked = "self_issued_jwt_revoked" // EventIntrospectionRequesterDenied is logged when /oauth/introspect is // called by a client that is neither the token's client_id nor on the // IntrospectionResourceServers allowlist. The endpoint returns // {"active": false} per RFC 7662 §2.2 so the denial is invisible to the // caller, but the audit record preserves the cross-client probe attempt. EventIntrospectionRequesterDenied = "introspection_requester_denied" // EventUserInfoServed is logged when /oauth/userinfo (OIDC Core 1.0 §5.3) // returns claims to an authenticated caller. The audit record carries the // subject and the scope-derived claim groups that were emitted so // operators can audit personal-data egress. EventUserInfoServed = "userinfo_served" // 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" // EventProviderNonceMismatch is logged when an upstream id_token's `nonce` // claim does not echo the value bound at /authorize. EventProviderNonceMismatch = "provider_nonce_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" // EventProviderTokenStorageFailed is logged when persisting the upstream // provider token (or its associated UserInfo) to the token store fails. // Dashboards key off this event to surface SSO-token-forwarding outages // that previously manifested only as silent post-auth breakage. EventProviderTokenStorageFailed = "provider_token_storage_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 MinKeyDistinctBytes = 16
MinKeyDistinctBytes is the minimum number of distinct byte values an AES-256 key must contain. 16 bounds the effective keyspace from a key drawn over this many symbols at ≥ 2^128 (the NIST minimum for AES). A uniformly random 32-byte key averages ~30 distinct bytes (σ ≈ 1.5), so the false-positive rate on genuine keys is negligible.
const RequestIDHeader = "X-Request-ID"
RequestIDHeader is the HTTP header for request IDs
Variables ¶
This section is empty.
Functions ¶
func DecodeKey ¶ added in v0.2.128
DecodeKey decodes an AES-256 token-encryption key from either base64 (what `openssl rand -base64 32` produces) or hex (`openssl rand -hex 32`). Detection tries base64 first and falls back to hex. The returned key is guaranteed to be 32 bytes.
DecodeKey is a convenience over KeyFromBase64 and KeyFromHex for operators who configure the same secret across tools that emit different encodings.
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 IsTrustedProxy ¶ added in v0.2.150
IsTrustedProxy reports whether the direct connection identified by remoteAddr (host:port or bare host) falls within one of the provided CIDRs. Use this to decide whether X-Forwarded-Proto/Host headers should be trusted for URL reconstruction (e.g. DPoP htu) when sitting behind a reverse proxy.
func KeyFromBase64 ¶
KeyFromBase64 decodes a base64-encoded encryption key
func KeyFromHex ¶ added in v0.2.109
KeyFromHex decodes a hex-encoded 32-byte encryption key. 64-char input is the canonical form produced by `openssl rand -hex 32`. Paired with KeyFromBase64 for operators who prefer one encoding over the other.
func KeyToBase64 ¶
KeyToBase64 encodes an encryption key to base64
func RateLimitBucket ¶ added in v0.2.126
RateLimitBucket returns the rate-limit key for an IP. IPv4 (including the IPv4-mapped IPv6 form `::ffff:a.b.c.d`) is canonicalized to its IPv4 string so both representations share a bucket. IPv6 returns the /64 prefix (the typical end-site allocation): without this, an attacker holding a /64 can rotate 2^64 addresses to bypass per-IP limits and defeat the CWE-307 closure. Unparseable inputs are returned verbatim so the limiter still keys on something stable.
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 SetAuditDropRecorder ¶ added in v0.2.124
func SetAuditDropRecorder(fn auditDropRecorder)
SetAuditDropRecorder registers a callback invoked whenever an audit event is dropped. Setting nil disables the hook. Intended to be called once at startup from instrumentation wiring code.
The hook is process-wide. Constructing a second [instrumentation.Instrumentation] re-registers it, so audit drops from any Auditor in the process route to the most recently constructed Instrumentation's meter — even if a different Auditor is attached to a different Server. Production deployments build one Server; tests that spin up multiple in parallel will see the second one win.
func SetDiscoveryCacheHeaders ¶ added in v0.2.134
func SetDiscoveryCacheHeaders(w http.ResponseWriter, maxAge time.Duration)
SetDiscoveryCacheHeaders overrides the Cache-Control + Pragma policy set by SetSecurityHeaders for the three discovery endpoints (/.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource, /.well-known/openid-configuration). RFC 8414 §3 and RFC 9728 §3 advertise these as cacheable; CDN-fronted deployments need a positive Cache-Control. Call after SetSecurityHeaders; the other headers are unchanged.
func SetEncryptionMetricRecorder ¶ added in v0.2.124
func SetEncryptionMetricRecorder(fn encryptionMetricRecorder)
SetEncryptionMetricRecorder registers a callback invoked on every completed Encrypt/Decrypt. Setting nil disables the hook (zero overhead when no metrics backend is wired). Safe to call from multiple goroutines.
The hook is process-wide. Constructing a second [instrumentation.Instrumentation] re-registers it, so Encrypt/Decrypt calls from any Encryptor in the process route to the most recently constructed Instrumentation's meter — even if a different Encryptor is attached to a different Server. Production deployments build one Server; tests that spin up multiple in parallel will see the second registration win.
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
func ValidateKeyEntropy ¶ added in v0.2.109
ValidateKeyEntropy rejects obvious placeholder keys (all-zero, repeated bytes, low-distinct-symbol patterns) that pass length checks but leave encrypted tokens trivially recoverable. Empty input is accepted — the caller has opted out of encryption entirely (see NewEncryptor).
This is a backstop against operator mistakes only. It cannot detect derived-but-weak keys (e.g. SHA-256 of a dictionary word averages ~27 distinct bytes). Generate real keys via GenerateKey or `openssl rand -hex 32`.
Types ¶
type Auditor ¶
type Auditor struct {
// contains filtered or unexported fields
}
Auditor handles security event logging with PII protection.
func NewAuditor ¶
func NewAuditor(logger *slog.Logger, enabled bool, opts ...AuditorOption) *Auditor
NewAuditor creates a new security auditor. opts apply after defaults.
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) LogEvent ¶
LogEvent logs a security event with hashed PII. Attributes are emitted under a single slog.Group named "audit" so consumers can split-route audit records from operational logs in their slog.Handler.
ctx is forwarded to the underlying slog.Handler; otelslog and similar trace-aware handlers use it to attach trace/span IDs to the record. Callers handling an HTTP request should pass r.Context(); background emissions should pass context.Background.
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
func (a *Auditor) LogSuspiciousActivity(ctx context.Context, userID, clientID, ipAddress, description string)
LogSuspiciousActivity logs suspicious activity
func (*Auditor) LogTokenIssued ¶
LogTokenIssued logs when a token is issued
func (*Auditor) LogTokenRefreshed ¶
func (a *Auditor) LogTokenRefreshed(ctx context.Context, userID, clientID, ipAddress string, rotated bool)
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)
type AuditorOption ¶ added in v0.2.135
type AuditorOption func(*Auditor)
AuditorOption configures an Auditor.
func WithPIIRedaction ¶ added in v0.2.135
func WithPIIRedaction(redact bool) AuditorOption
WithPIIRedaction toggles hashing of the auxiliary identity fields (client_id, ip_address, user_agent) in every audit record. The user_id is hashed regardless. Default off — existing slog sinks continue to receive cleartext values. Enable for sinks routed to a regulated (PCI, HIPAA, GDPR) store where compliance demands minimisation of auxiliary PII.
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()
func (*ClientRegistrationRateLimiter) Window ¶ added in v0.2.126
func (rl *ClientRegistrationRateLimiter) Window() time.Duration
Window returns the configured time window. Callers computing a Retry-After hint can use this as an upper bound for when the bucket will refill.
type Encryptor ¶
type Encryptor struct {
// contains filtered or unexported fields
}
Encryptor handles token encryption at rest using AES-256-GCM. Emits v1 envelopes; decrypts v0 (legacy) and v1.
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 and must pass ValidateKeyEntropy — a low-entropy key is rejected rather than silently encrypting all tokens under a trivially-known secret.
func (*Encryptor) Decrypt ¶
Decrypt decrypts a base64-encoded envelope. Accepts both v1 (0x01 || kid || nonce || ct) and legacy v0 (nonce || ct).
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 KeyRing ¶ added in v0.2.135
type KeyRing interface {
// ActiveKID returns the kid byte stamped into newly-emitted ciphertexts.
ActiveKID() byte
// AEAD returns the AEAD primitive bound to the given kid. The returned
// AEAD's NonceSize() and Overhead() must be stable across calls.
AEAD(kid byte) (cipher.AEAD, error)
}
KeyRing resolves AES-256 keys keyed by a 1-byte `kid`. `ActiveKID` returns the id stamped into every new ciphertext; `AEAD(kid)` resolves any historical id present in stored rows. The built-in NewEncryptor wires a [singleKeyRing] with kid = 0 because in-tree consumers need exactly one key today.
Multi-key consumers (external KMS, scheduled rotation, dual-write windows) supply their own implementation. Notes for those implementations:
- `ActiveKID` is invoked on every Encrypt; implementations that want to change the active id at runtime MUST make this call goroutine-safe. The built-in `singleKeyRing` returns a constant and is trivially safe.
- The Encryptor does not yet expose which kid authenticated a given decrypted row, so re-encrypt-on-read rotation is not implementable against today's Encryptor.Decrypt. Tracked in #335.
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) Rate ¶ added in v0.2.126
func (rl *RateLimiter) Rate() int
Rate returns the configured requests-per-second. Callers computing a Retry-After hint should treat 0 as "unspecified" and fall back to a constant.
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