Documentation
¶
Overview ¶
Package core provides the high-level authentication and authorization business logic. It orchestrates the low-level data operations from the auth package and adds features like password hashing, session management, rate limiting, audit logging, and more.
The core package is organized around specialized service objects:
- AuthService: Main orchestrator that coordinates all sub-services
- UserService: User identity management
- AccountService: Provider-specific account management and authentication
- SessionService: Session creation, validation, and caching
- VerificationService: Token generation and validation for email/password flows
All services accept context for cancellation and use the audit logger for security event tracking.
Index ¶
- Constants
- Variables
- func AegisContextMiddleware() func(http.Handler) http.Handler
- func AuthMiddleware(sessionService *SessionService) func(http.Handler) http.Handler
- func Authenticated(ctx context.Context) bool
- func BindAndValidate[T interface{ ... }](r *http.Request) (T, error)
- func ClampIntToInt32(n int) int32
- func DeriveSecret(masterSecret []byte, purpose string, length int) []byte
- func ExtendUser(ctx context.Context, key string, value any)
- func GenerateID() string
- func GenerateOTPCode(length int) (string, error)
- func GetClientIP(r *http.Request) string
- func GetIPAddress(ctx context.Context) string
- func GetPathParam(r *http.Request, name string) string
- func GetPluginValue(ctx context.Context, key string) any
- func GetRequestID(ctx context.Context) string
- func GetSanitizedPathParam(r *http.Request, name string) string
- func GetSession(ctx context.Context) *auth.Session
- func GetUser(ctx context.Context) (*auth.User, error)
- func GetUserAgent(ctx context.Context) string
- func GetUserExtension(ctx context.Context, key string) any
- func GetUserExtensionBool(ctx context.Context, key string) bool
- func GetUserExtensionString(ctx context.Context, key string) string
- func GetUserID(ctx context.Context) string
- func HasSession(ctx context.Context) bool
- func HashPassword(password string, time, memory uint32, threads uint8, keyLen uint32) (string, error)
- func HashShort(s string) string
- func IsAuthError(err error) bool
- func IsContextInitialized(ctx context.Context) bool
- func IsValidationError(err error) bool
- func MaxBodySizeMiddleware(maxBytes int64) func(http.Handler) http.Handler
- func MustGetUser(ctx context.Context) *auth.User
- func NormalizeWhitespace(input string) string
- func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler
- func RedactForLog(s string) string
- func RequireAuthMiddleware(_ *SessionService) func(http.Handler) http.Handler
- func SanitizeEmail(email string) string
- func SanitizeFilename(filename string) string
- func SanitizeHTML(content string) string
- func SanitizeMultiline(input string, maxLength int) string
- func SanitizePhoneNumber(phone string) string
- func SanitizeSQL(input string) string
- func SanitizeString(input string, config *SanitizationConfig) string
- func SanitizeURL(url string) string
- func SanitizeUsername(username string, maxLength int) string
- func SetCustomIDGenerator(generator IDGeneratorFunc)
- func SetHTTPLogger(l HTTPLogger)
- func SetIDStrategy(strategy IDStrategy)
- func SetPluginValue(ctx context.Context, key string, value any)
- func StripTags(input string) string
- func ValidateEmail(email string) error
- func ValidateMiddleware[T interface{ ... }](handler func(w http.ResponseWriter, r *http.Request, req T)) http.HandlerFunc
- func ValidatePassword(password string, policy *PasswordPolicyConfig) error
- func ValidatePasswordSimple(password string, minLength int) error
- func VerifyPassword(password, encodedHash string) (bool, error)
- func WithContextInitialized(ctx context.Context) context.Context
- func WithEnrichedUser(ctx context.Context, eu *EnrichedUser) context.Context
- func WithPathParamFunc(ctx context.Context, fn PathParamFunc) context.Context
- func WithPluginData(ctx context.Context, pd *PluginData) context.Context
- func WithRequestID(ctx context.Context, requestID string) context.Context
- func WithRequestMeta(ctx context.Context, meta *RequestMeta) context.Context
- func WithSession(ctx context.Context, session *auth.Session) context.Context
- func WithUser(ctx context.Context, user *auth.User) context.Context
- func WrapError(err error, message string) error
- func WriteJSON(w http.ResponseWriter, statusCode int, data any)
- func WriteJSONError(w http.ResponseWriter, statusCode int, message string)
- type AccountModel
- type AccountService
- func (s *AccountService) CreateAccount(ctx context.Context, account auth.Account) error
- func (s *AccountService) DeleteAccount(ctx context.Context, id string) error
- func (s *AccountService) GetAccountByID(ctx context.Context, id string) (auth.Account, error)
- func (s *AccountService) GetAccountsByUserID(ctx context.Context, userID string) ([]auth.Account, error)
- func (s *AccountService) GetPasswordAccount(ctx context.Context, userID string) (auth.Account, error)
- func (s *AccountService) UpdatePassword(ctx context.Context, userID, newPassword string) error
- func (s *AccountService) VerifyPassword(ctx context.Context, userID, password string) (bool, error)
- type AegisContext
- func (ac *AegisContext) Context() context.Context
- func (ac *AegisContext) WithExtension(key string, value any) *AegisContext
- func (ac *AegisContext) WithPluginData() *AegisContext
- func (ac *AegisContext) WithRequestID(id string) *AegisContext
- func (ac *AegisContext) WithRequestMeta(meta *RequestMeta) *AegisContext
- func (ac *AegisContext) WithSession(session *auth.Session) *AegisContext
- func (ac *AegisContext) WithUser(user *auth.User) *AegisContext
- type AuditEvent
- type AuditEventType
- type AuditLogger
- type AuthConfig
- type AuthError
- type AuthService
- type CookieManager
- func (cm *CookieManager) ClearSessionCookie(w http.ResponseWriter)
- func (cm *CookieManager) GetConfig() *SessionConfig
- func (cm *CookieManager) GetCookie(r *http.Request, name string) (string, error)
- func (cm *CookieManager) GetSessionCookie(r *http.Request) (string, error)
- func (cm *CookieManager) GetSessionCookieName() string
- func (cm *CookieManager) SetCookie(w http.ResponseWriter, name, value string, maxAge time.Duration)
- func (cm *CookieManager) SetCustomCookie(w http.ResponseWriter, opts CookieOptions)
- func (cm *CookieManager) SetSessionCookie(w http.ResponseWriter, token string)
- type CookieOptions
- type CookieSettings
- type EmailPasswordHandlers
- type EnrichedUser
- func (eu *EnrichedUser) Get(key string) any
- func (eu *EnrichedUser) GetBool(key string) bool
- func (eu *EnrichedUser) GetMap(key string) map[string]any
- func (eu *EnrichedUser) GetString(key string) string
- func (eu *EnrichedUser) GetStringSlice(key string) []string
- func (eu *EnrichedUser) Has(key string) bool
- func (eu *EnrichedUser) Keys() []string
- func (eu *EnrichedUser) MarshalJSON() ([]byte, error)
- func (eu *EnrichedUser) Set(key string, value any)
- func (eu *EnrichedUser) ToAPIResponse() map[string]any
- func (eu *EnrichedUser) ToAPIResponseFiltered(config *UserFieldsConfig) map[string]any
- type HTTPLogger
- type IDGeneratorFunc
- type IDStrategy
- type KeyManager
- type LoggerAuditLogger
- type LoginAttemptConfig
- type LoginAttemptTracker
- func (lat *LoginAttemptTracker) ClearAttempts(ctx context.Context, identifier string) error
- func (lat *LoginAttemptTracker) IsLockedOut(ctx context.Context, identifier string) (bool, time.Duration, error)
- func (lat *LoginAttemptTracker) RecordFailedAttempt(ctx context.Context, identifier string) (int, bool, error)
- func (lat *LoginAttemptTracker) Stop()
- type LoginRequest
- type LoginResult
- type NoOpAuditLogger
- type PaginationParams
- type PasswordHasherConfig
- type PasswordPolicyConfig
- type PathParamFunc
- type PluginData
- func (pd *PluginData) Delete(key string)
- func (pd *PluginData) Get(key string) any
- func (pd *PluginData) GetBool(key string) bool
- func (pd *PluginData) GetString(key string) string
- func (pd *PluginData) Has(key string) bool
- func (pd *PluginData) Keys() []string
- func (pd *PluginData) Set(key string, value any)
- type RateLimitConfig
- type RateLimiter
- type RedisConfig
- type RedisKeyManager
- type RegisterRequest
- type RegisterResult
- type RequestBodyMeta
- type RequestMeta
- type Response
- type ResponseMeta
- type RouteMetadata
- type SanitizationConfig
- type SchemaRequirement
- type SchemaValidator
- type SessionConfig
- type SessionModel
- type SessionService
- func (s *SessionService) CreateSession(ctx context.Context, user *auth.User) (*auth.Session, error)
- func (s *SessionService) DeleteSession(ctx context.Context, token string) error
- func (s *SessionService) DeleteUserSessions(ctx context.Context, userID string) error
- func (s *SessionService) EnableBearerAuth()
- func (s *SessionService) GetConfig() *SessionConfig
- func (s *SessionService) GetCookieManager() *CookieManager
- func (s *SessionService) GetRedisClient() *redis.Client
- func (s *SessionService) GetUserSessions(ctx context.Context, userID string) ([]*auth.Session, error)
- func (s *SessionService) IsBearerAuthEnabled() bool
- func (s *SessionService) Logout(ctx context.Context, token string) error
- func (s *SessionService) RefreshSession(ctx context.Context, refreshToken string) (*auth.Session, error)
- func (s *SessionService) RevokeSessionByID(ctx context.Context, userID, sessionID string) error
- func (s *SessionService) ValidateSession(ctx context.Context, tokenString string) (*auth.Session, *auth.User, error)
- type SessionWithUser
- type StaticKeyManager
- type UserFieldsConfig
- type UserModel
- type UserService
- func (s *UserService) CreateUser(ctx context.Context, user auth.User, password string) (auth.User, error)
- func (s *UserService) CreateUserWithEmail(ctx context.Context, name, email, password string) (auth.User, error)
- func (s *UserService) CreateUserWithoutPassword(ctx context.Context, user auth.User) (auth.User, error)
- func (s *UserService) DeleteUser(ctx context.Context, id string) error
- func (s *UserService) GetUserByEmail(ctx context.Context, email string) (auth.User, error)
- func (s *UserService) GetUserByID(ctx context.Context, id string) (auth.User, error)
- func (s *UserService) UpdateUser(ctx context.Context, user auth.User) error
- func (s *UserService) UpdateUserEmail(ctx context.Context, userID, email string) error
- type ValidationError
- type ValidationErrors
- type VerificationModel
- type VerificationService
- func (s *VerificationService) CreateVerification(ctx context.Context, identifier, vType string, expiry time.Duration, ...) (auth.Verification, error)
- func (s *VerificationService) DeleteVerification(ctx context.Context, id string) error
- func (s *VerificationService) InvalidateVerification(ctx context.Context, identifier, vType string) error
- func (s *VerificationService) ValidateVerification(ctx context.Context, token string) (auth.Verification, error)
Constants ¶
const ( // DefaultRateLimitRequests is the default number of requests allowed per window // for general API endpoints (100 requests/minute is suitable for most applications) DefaultRateLimitRequests = 100 // DefaultRateLimitWindow is the default time window for rate limiting (1 minute) DefaultRateLimitWindow = time.Minute // DefaultRateLimitKeyPrefix is the default Redis key prefix for rate limit counters DefaultRateLimitKeyPrefix = "aegis:ratelimit:" // AuthRateLimitRequests is a stricter limit for authentication endpoints // (10 requests/minute prevents brute force while allowing legitimate retries) AuthRateLimitRequests = 10 // AuthRateLimitKeyPrefix is the Redis key prefix for auth-specific rate limits AuthRateLimitKeyPrefix = "aegis:ratelimit:auth:" )
Default rate limiting constants control API request throttling. Separate limits are defined for general endpoints vs. authentication endpoints.
const ( // DefaultMaxLoginAttempts is the maximum number of failed login attempts allowed // before triggering account lockout (5 attempts is OWASP-recommended) DefaultMaxLoginAttempts = 5 // DefaultLoginLockoutDuration is how long to lock out after max attempts // (15 minutes balances security with user convenience) DefaultLoginLockoutDuration = 15 * time.Minute // DefaultLoginAttemptWindow is the time window for counting attempts // (attempts older than this are not counted toward the limit) DefaultLoginAttemptWindow = 15 * time.Minute )
Default login attempt tracking constants prevent brute force attacks. After exceeding max attempts, accounts are temporarily locked.
const ( // DefaultPasswordMinLength is the minimum password length (8 characters) // NIST recommends 8+ characters for user-chosen passwords DefaultPasswordMinLength = 8 // DefaultPasswordMaxLength is the maximum password length (128 characters) // Prevents DoS attacks via extremely long password hashing. // 0 would mean no limit. DefaultPasswordMaxLength = 128 )
Default password policy constants enforce minimum security requirements.
const ( // DefaultSessionExpiry is the default session expiration time (24 hours) // After this, users must re-authenticate or use refresh token DefaultSessionExpiry = 24 * time.Hour // DefaultRefreshExpiry is the default refresh token expiration time (7 days) // Allows "remember me" functionality while limiting token lifetime DefaultRefreshExpiry = 7 * 24 * time.Hour )
Default session configuration constants control session lifetimes.
const ( // DefaultArgon2Time is the number of iterations (time cost) // Higher = slower hashing = more brute force resistant DefaultArgon2Time = 1 // DefaultArgon2Memory is the memory cost in KiB (64 MB) // Higher memory makes GPU attacks less effective DefaultArgon2Memory = 64 * 1024 // DefaultArgon2Threads is the degree of parallelism // Should match typical server CPU core count DefaultArgon2Threads = 4 // DefaultArgon2KeyLength is the derived key length in bytes (256 bits) DefaultArgon2KeyLength = 32 )
Default password hashing constants use Argon2id parameters. These values are based on OWASP recommendations for 2024 and balance security (resistance to attacks) with performance (server load).
const ( // TokenLength is the length of generated tokens in bytes (32 bytes = 256 bits) // Provides sufficient entropy to prevent guessing attacks TokenLength = 32 // SaltLength is the length of password salt in bytes (16 bytes = 128 bits) // Ensures unique hash outputs even for identical passwords SaltLength = 16 )
Token generation constants define entropy for cryptographic tokens.
const ( // RedisSessionPrefix is the Redis key prefix for session storage RedisSessionPrefix = "aegis:session:" // RedisRefreshTokenPrefix is the Redis key prefix for refresh tokens RedisRefreshTokenPrefix = "aegis:refresh:" // RedisUserSessionsPrefix is the Redis key prefix for user session sets // (used to track all sessions for a user for "logout all devices") RedisUserSessionsPrefix = "aegis:user_sessions:" // RedisLoginAttemptsPrefix is the Redis key prefix for login attempt counters RedisLoginAttemptsPrefix = "aegis:login_attempts:" )
Redis key prefixes prevent key collisions when using shared Redis instances. All Aegis keys are prefixed with "aegis:" for easy identification.
const ( // DefaultCookieName is the default session cookie name DefaultCookieName = "aegis_session" // DefaultCookiePath is the default cookie path DefaultCookiePath = "/" // DefaultCookieSameSite is the default SameSite attribute DefaultCookieSameSite = "Lax" // DefaultCookieHTTPOnly is the default HttpOnly attribute DefaultCookieHTTPOnly = true // DefaultCookieSecure is the default Secure attribute // Ensures cookies are only sent over HTTPS in production DefaultCookieSecure = true )
Default cookie settings for session management.
const ( // UppercaseStart is the start of uppercase ASCII range UppercaseStart = 'A' // UppercaseEnd is the end of uppercase ASCII range UppercaseEnd = 'Z' // LowercaseStart is the start of lowercase ASCII range LowercaseStart = 'a' // LowercaseEnd is the end of lowercase ASCII range LowercaseEnd = 'z' // DigitStart is the start of digit ASCII range DigitStart = '0' // DigitEnd is the end of digit ASCII range DigitEnd = '9' // SpecialRange1Start is the start of first special character range SpecialRange1Start = '!' // SpecialRange1End is the end of first special character range SpecialRange1End = '/' // SpecialRange2Start is the start of second special character range SpecialRange2Start = ':' // SpecialRange2End is the end of second special character range SpecialRange2End = '@' // SpecialRange3Start is the start of third special character range SpecialRange3Start = '[' // SpecialRange3End is the end of third special character range SpecialRange3End = '`' // SpecialRange4Start is the start of fourth special character range SpecialRange4Start = '{' // SpecialRange4End is the end of fourth special character range SpecialRange4End = '~' )
Character range constants for password validation
const ( // AuthErrorCodeInvalidCredentials indicates wrong username/password // #nosec G101 AuthErrorCodeInvalidCredentials = "INVALID_CREDENTIALS" // AuthErrorCodeUserNotFound indicates user does not exist AuthErrorCodeUserNotFound = "USER_NOT_FOUND" // AuthErrorCodeUserDisabled indicates account is deactivated AuthErrorCodeUserDisabled = "USER_DISABLED" // AuthErrorCodeAccountNotFound indicates no account for this provider AuthErrorCodeAccountNotFound = "ACCOUNT_NOT_FOUND" // AuthErrorCodeTokenInvalid indicates malformed token AuthErrorCodeTokenInvalid = "TOKEN_INVALID" // AuthErrorCodeTokenExpired indicates token lifetime exceeded AuthErrorCodeTokenExpired = "TOKEN_EXPIRED" // AuthErrorCodeSessionInvalid indicates invalid session AuthErrorCodeSessionInvalid = "SESSION_INVALID" // AuthErrorCodeRateLimit indicates too many requests AuthErrorCodeRateLimit = "RATE_LIMIT" AuthErrorCodeUnauthorized = "UNAUTHORIZED" // AuthErrorCodeInternal indicates an unexpected server error AuthErrorCodeInternal = "INTERNAL_ERROR" )
Predefined auth error codes for API responses. These codes provide stable identifiers that clients can programmatically handle without parsing error messages.
const ( // DefaultMaxBodySize is the default maximum request body size (1MB) // Suitable for most API endpoints with JSON payloads DefaultMaxBodySize int64 = 1 << 20 // 1 MB // MaxBodySizeSmall is for endpoints with small payloads like login (64KB) // Use for authentication endpoints to prevent abuse MaxBodySizeSmall int64 = 64 << 10 // 64 KB // MaxBodySizeLarge is for endpoints that may have larger payloads (10MB) // Use for file uploads or bulk operations MaxBodySizeLarge int64 = 10 << 20 // 10 MB )
Default request body size limits prevent denial-of-service attacks via extremely large request bodies. These limits can be overridden per-route.
const ( // Core entity schemas SchemaUser = "User" SchemaEnrichedUser = "EnrichedUser" SchemaSession = "Session" SchemaSessionWithUser = "SessionWithUser" // Common response schemas SchemaError = "Error" SchemaSuccess = "Success" // Request schemas SchemaRefreshTokenRequest = "RefreshTokenRequest" SchemaLoginRequest = "LoginRequest" SchemaRegisterRequest = "RegisterRequest" // List schemas SchemaSessionList = "SessionList" )
Core schema names for OpenAPI documentation.
These constants provide type-safe references to OpenAPI schema names used throughout the Aegis framework. They are used by:
- RouteMetadata: Annotating HTTP handlers with request/response schemas
- OpenAPI plugin: Generating OpenAPI 3.0 specifications
- API documentation: Auto-generating API docs from code
Benefits of using constants:
- Type safety: Compile-time checking (no typos in schema names)
- Refactoring: Easy to rename schemas across the codebase
- Discoverability: IDE autocomplete shows available schemas
Naming convention:
- Singular for entities: SchemaUser, not SchemaUsers
- Descriptive suffixes: SchemaUserList for lists, SchemaLoginRequest for requests
Example usage in RouteMetadata:
metadata := &core.RouteMetadata{
RequestBody: &core.RequestBodyMeta{
Schema: core.SchemaLoginRequest,
},
Responses: map[string]*core.ResponseMeta{
"200": {Schema: core.SchemaUser},
"401": {Schema: core.SchemaError},
},
}
const DefaultSecretLength = 32
DefaultSecretLength is the recommended length for derived secrets (256 bits / 32 bytes).
This provides 256-bit security, which is the standard for symmetric encryption and HMAC operations. Use this constant when calling DeriveSecret:
secret := core.DeriveSecret(master, "purpose", core.DefaultSecretLength)
const EmailRegexPattern = `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`
EmailRegexPattern is the regex pattern for email validation (RFC 5322 simplified)
const (
// PasswordProvider is the provider name for password authentication
PasswordProvider = "password"
)
Provider constants
Variables ¶
var ( // ErrUserNotFound indicates the requested user does not exist ErrUserNotFound = errors.New("user not found") // ErrInvalidCredentials indicates incorrect username/password combination ErrInvalidCredentials = errors.New("invalid credentials") // ErrUserDisabled indicates the user account has been deactivated ErrUserDisabled = errors.New("user account is disabled") // ErrEmailNotVerified indicates email verification is required ErrEmailNotVerified = errors.New("email not verified") // ErrInvalidToken indicates a malformed or invalid token ErrInvalidToken = errors.New("invalid token") // ErrTokenExpired indicates the token has exceeded its lifetime ErrTokenExpired = errors.New("token expired") // ErrSessionNotFound indicates the session does not exist ErrSessionNotFound = errors.New("session not found") // ErrInvalidSession indicates a malformed or corrupted session ErrInvalidSession = errors.New("invalid session") // ErrSessionExpired indicates the session has exceeded its lifetime ErrSessionExpired = errors.New("session expired") // ErrRateLimitExceeded indicates too many requests from this client ErrRateLimitExceeded = errors.New("rate limit exceeded") // ErrInvalidRequest indicates malformed request data ErrInvalidRequest = errors.New("invalid request") ErrUnauthorized = errors.New("unauthorized") // ErrForbidden indicates the authenticated user lacks permissions ErrForbidden = errors.New("forbidden") // ErrInternalServer indicates an unexpected server error ErrInternalServer = errors.New("internal server error") // ErrDatabaseConnection indicates database connectivity issues ErrDatabaseConnection = errors.New("database connection error") // ErrRedisConnection indicates Redis connectivity issues ErrRedisConnection = errors.New("redis connection error") )
Common authentication errors used throughout the framework. These sentinel errors can be compared using errors.Is() and provide consistent error handling across the application.
Functions ¶
func AegisContextMiddleware ¶
AegisContextMiddleware initializes the Aegis request context for each HTTP request.
This middleware is called automatically by the Aegis framework and sets up:
- Request ID: Unique identifier for tracing and logging
- Request metadata: IP address, user agent, method, path
- Plugin data store: Key-value storage for plugins to share data
- Initialization marker: Prevents double initialization
The middleware is idempotent - if the context is already initialized (e.g., by a parent middleware), it skips initialization and passes through.
Users typically don't need to call this directly - it's integrated into the Aegis HTTP stack automatically.
func AuthMiddleware ¶
func AuthMiddleware(sessionService *SessionService) func(http.Handler) http.Handler
AuthMiddleware returns HTTP middleware that validates sessions and injects authenticated user data into the request context.
Authentication flow:
- Check if context already initialized (if not, initialize it)
- Look for session in per-request cache (avoid redundant lookups)
- Extract session token from cookie or Authorization header
- Validate the session token (database + Redis cache)
- Load the associated user
- Create EnrichedUser and populate with plugin extensions
- Inject user and session into context
After this middleware, authenticated requests can access:
- core.GetUser(ctx) - Get the base user
- core.GetEnrichedUser(ctx) - Get user with plugin extensions
- core.GetSession(ctx) - Get the current session
Unauthenticated requests continue normally (no user/session in context). Protected routes should check for authentication and return 401 if missing.
The middleware caches the session in the request context to avoid redundant database/Redis lookups if multiple handlers need authentication data.
Parameters:
- sessionService: Service for session validation and user lookup
Example usage:
protectedRouter.Use(core.AuthMiddleware(sessionService))
func Authenticated ¶
Authenticated checks if the context has an authenticated user.
func BindAndValidate ¶
BindAndValidate decodes a JSON request body and validates it. T must implement a Validate() error method. This helper ensures consistent validation across all handlers.
Example usage:
req, err := core.BindAndValidate[CreateOrganizationRequest](r)
if err != nil {
core.WriteValidationError(w, err)
return
}
func ClampIntToInt32 ¶
ClampIntToInt32 safely converts an `int` to `int32` by clamping the value to the valid range for int32. This prevents unsafe downcasts on platforms where `int` is larger than 32 bits (e.g., amd64) and guards against potential overflows when values originate from untrusted sources.
func DeriveSecret ¶
DeriveSecret derives a purpose-specific secret from a master secret using HKDF-SHA256.
HKDF (HMAC-based Key Derivation Function) is a cryptographic key derivation function that allows safely deriving multiple purpose-specific keys from a single master secret.
Why use HKDF instead of reusing the master secret?
- Cryptographic separation: Each purpose gets a unique, independent key
- Security isolation: Compromise of one derived key doesn't affect others
- Standard practice: Recommended by NIST SP 800-108 and RFC 5869
Common use cases in Aegis:
- CSRF token signing
- OAuth state token encryption
- JWT signing keys
- Cookie encryption keys
- API key derivation
Parameters:
- masterSecret: The master secret (minimum 32 bytes recommended)
- purpose: Unique identifier for this key's purpose (e.g., "csrf", "oauth-state", "jwt")
- length: Output length in bytes (typically 32 for 256-bit keys)
Security notes:
- Different purposes MUST use different purpose strings
- Master secret should be cryptographically random (32+ bytes)
- Output keys are deterministic (same inputs → same output)
Example:
masterSecret := []byte("your-32-byte-master-secret-here!")
// Derive separate keys for different purposes
csrfSecret := core.DeriveSecret(masterSecret, "csrf", 32)
oauthSecret := core.DeriveSecret(masterSecret, "oauth-state", 32)
jwtSecret := core.DeriveSecret(masterSecret, "jwt-signing", 32)
func ExtendUser ¶
ExtendUser adds data to the enriched user in context. If no enriched user exists, this is a no-op. Plugins should call this in their middleware or handlers to add their data.
Example:
core.ExtendUser(ctx, "admin:role", "admin")
core.ExtendUser(ctx, "orgs:memberships", []string{"org1", "org2"})
func GenerateID ¶
func GenerateID() string
GenerateID generates a unique identifier using the configured ID strategy.
This is the primary ID generation function used throughout Aegis for:
- User IDs
- Session IDs
- Account IDs
- Verification token IDs
Default Strategy: ULID
- Format: "01ARZ3NDEKTSV4RRFFQ69G5FAV" (26 characters)
- Sortable by creation time
- Database-friendly indexing
- No configuration required
Strategy Selection:
ULID (default): Best for most use cases
Sortable IDs improve database performance
Compact format (26 chars vs 36 for UUID)
Built-in timestamp makes debugging easier
UUID: When you need standard UUID format
Format: "550e8400-e29b-41d4-a716-446655440000"
Maximum randomness (122 bits)
Not sortable (random ordering)
Custom: For specialized requirements
Implement IDGeneratorFunc
Examples: KSUID, Snowflake, nanoid, database sequences
Usage Examples:
// Default (ULID)
userID := core.GenerateID() // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// Switch to UUID
core.SetIDStrategy(core.IDStrategyUUID)
userID := core.GenerateID() // "550e8400-e29b-41d4-a716-446655440000"
// Use custom generator
core.SetCustomIDGenerator(func() string { return ksuid.New().String() })
userID := core.GenerateID() // Custom format
Note: For database-generated IDs (SERIAL, AUTO_INCREMENT), configure your database schema to generate IDs and don't call this function.
func GenerateOTPCode ¶
GenerateOTPCode generates a random numeric OTP (One-Time Password) code.
The code uses cryptographically secure randomness (crypto/rand) and includes leading zeros to ensure the specified length.
Parameters:
- length: Number of digits (typically 4-8)
Common lengths:
- 6 digits: Standard for most 2FA systems (Google Authenticator, etc.)
- 4 digits: Short codes for SMS (balance security vs user convenience)
- 8 digits: High-security scenarios
Returns a numeric string with leading zeros if necessary.
Example:
// Generate 6-digit code code, _ := core.GenerateOTPCode(6) // "042816", "912345", etc. // Generate 4-digit code for SMS code, _ := core.GenerateOTPCode(4) // "0042", "9123", etc.
func GetClientIP ¶
GetClientIP extracts the client IP address from the request. It checks headers in order: X-Forwarded-For, X-Real-IP, then falls back to RemoteAddr.
func GetIPAddress ¶
GetIPAddress extracts the IP address from context metadata. Falls back to extracting from request if not in context.
func GetPathParam ¶
GetPathParam extracts a path parameter from the request using the router's path param function stored in context. Falls back to Go 1.22+ PathValue.
func GetPluginValue ¶
GetPluginValue is a convenience function to get a plugin value directly from context. Returns nil if plugin data is not initialized or key doesn't exist.
func GetRequestID ¶
GetRequestID extracts the request ID from the context. Returns empty string if not set.
func GetSanitizedPathParam ¶
GetSanitizedPathParam extracts and sanitizes a path parameter. This is the recommended way to retrieve IDs and other path parameters from the URL path.
func GetSession ¶
GetSession extracts the session from the context. Returns nil if no session is present. This acts as a per-request cache - once a session is stored in context, it can be retrieved without hitting the database or Redis again.
func GetUser ¶
GetUser extracts the user from the context. Returns an error if no user is present (not authenticated).
func GetUserAgent ¶
GetUserAgent extracts the user agent from context metadata.
func GetUserExtension ¶
GetUserExtension retrieves a specific extension from the enriched user. Returns nil if user is not authenticated or extension doesn't exist.
func GetUserExtensionBool ¶
GetUserExtensionBool retrieves a bool extension from the enriched user.
func GetUserExtensionString ¶
GetUserExtensionString retrieves a string extension from the enriched user.
func GetUserID ¶
GetUserID is a convenience function to get just the user ID from context. Returns empty string if not authenticated.
func HasSession ¶
HasSession checks if a session exists in context (already validated for this request). Use this to avoid redundant session validation within the same request.
func HashPassword ¶
func HashPassword(password string, time, memory uint32, threads uint8, keyLen uint32) (string, error)
HashPassword creates a secure password hash using the Argon2id algorithm.
The function generates a cryptographically random salt and derives a hash from the password using the specified parameters. The result is encoded in PHC string format, which includes all parameters needed for later verification.
Parameters:
- password: The plaintext password to hash
- time: Number of iterations (higher = slower but more secure). Use 0 for defaults.
- memory: Memory usage in KiB (higher = more RAM needed). Use 0 for defaults.
- threads: Degree of parallelism (typically CPU core count). Use 0 for defaults.
- keyLen: Length of the derived key in bytes. Use 0 for defaults.
Returns a PHC-formatted string like:
$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
This format is portable and includes the version, parameters, salt, and hash, allowing verification even if default parameters change in the future.
Example:
hash, err := HashPassword("my-secret-password", 0, 0, 0, 0)
// hash = "$argon2id$v=19$m=65536,t=3,p=4$..."
func HashShort ¶
HashShort returns a short hash string (hex) of the input suitable for non-reversible identification in logs.
func IsContextInitialized ¶
IsContextInitialized checks if AegisContextMiddleware has been run. This is used internally to ensure proper middleware chain ordering.
func IsValidationError ¶
IsValidationError checks if an error is a validation error
func MaxBodySizeMiddleware ¶
MaxBodySizeMiddleware returns middleware that limits request body size. This helps prevent DoS attacks via large request bodies.
Example:
router.Use(core.MaxBodySizeMiddleware(core.DefaultMaxBodySize))
func MustGetUser ¶
MustGetUser extracts the user from context, panicking if not found. Use this only in handlers where authentication is guaranteed by middleware.
func NormalizeWhitespace ¶
NormalizeWhitespace collapses multiple spaces into single spaces.
Example:
text := core.NormalizeWhitespace("Hello World \n Test")
// Output: "Hello World Test"
func RateLimitMiddleware ¶
func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler
RateLimitMiddleware creates a middleware that rate limits requests
func RedactForLog ¶
RedactForLog returns a masked version of a user-provided identifier suitable for inclusion in logs. It preserves a small, non-sensitive hint while removing the majority of the value to avoid leaking sensitive data.
Examples:
- email: "[email protected]" -> "a***@example.com"
- phone: "+1234567890" -> "+1******90"
- other strings: "userid-abcdef" -> "us***ef"
func RequireAuthMiddleware ¶
func RequireAuthMiddleware(_ *SessionService) func(http.Handler) http.Handler
RequireAuthMiddleware returns middleware that requires authentication.
func SanitizeEmail ¶
SanitizeEmail sanitizes and normalizes email addresses.
Email-specific sanitization:
- Converts to lowercase (emails are case-insensitive)
- Removes whitespace
- Removes dangerous characters
- Validates basic format
Note: This does NOT validate email format. Use ValidateEmail() for validation.
Example:
email := core.SanitizeEmail(" [email protected] ")
// Output: "[email protected]"
func SanitizeFilename ¶
SanitizeFilename sanitizes filenames to prevent directory traversal attacks.
Filename-specific sanitization:
- Removes path separators (/, \)
- Removes null bytes
- Removes control characters
- Blocks directory traversal patterns (.., .)
- Enforces length limits
Example:
filename := core.SanitizeFilename("../../etc/passwd")
// Output: "etcpasswd"
filename := core.SanitizeFilename("my<file>.txt")
// Output: "myfile.txt"
func SanitizeHTML ¶
SanitizeHTML sanitizes HTML content for safe display.
This function escapes HTML entities to prevent XSS attacks while preserving the original text content. Use this when you need to display user-generated content in HTML context.
Example:
content := core.SanitizeHTML("<script>alert('xss')</script>")
// Output: "<script>alert('xss')</script>"
func SanitizeMultiline ¶
SanitizeMultiline sanitizes multi-line text input.
Multiline-specific sanitization:
- Preserves newlines and basic formatting
- Removes dangerous HTML/scripts
- Normalizes line endings to \n
- Limits total length
Use this for text areas, descriptions, and comments.
Example:
text := core.SanitizeMultiline("Line 1\r\nLine 2\r\n<script>alert('xss')</script>", 5000)
// Output: "Line 1\nLine 2\n"
func SanitizePhoneNumber ¶
SanitizePhoneNumber sanitizes phone numbers to a consistent format.
Phone-specific sanitization:
- Keeps only digits, plus sign, and hyphens
- Removes all other characters
- Trims whitespace
Example:
phone := core.SanitizePhoneNumber("+1 (555) 123-4567")
// Output: "+1-555-123-4567"
func SanitizeSQL ¶
SanitizeSQL removes common SQL injection patterns.
WARNING: This is NOT a replacement for parameterized queries! Always use prepared statements or parameterized queries for SQL. This function is only a defense-in-depth measure.
Removes:
- SQL comments (-- , #, /* */)
- Semicolons (statement terminators)
- Null bytes
Example:
input := core.SanitizeSQL("admin' OR '1'='1' --")
// Output: "admin' OR '1'='1' "
func SanitizeString ¶
func SanitizeString(input string, config *SanitizationConfig) string
SanitizeString performs general-purpose string sanitization.
This is the primary sanitization function for most user inputs like names, descriptions, and general text fields. It applies multiple security measures:
- Removes null bytes (prevents null byte injection)
- Strips HTML tags (prevents XSS)
- Removes control characters (prevents terminal injection)
- Normalizes whitespace (improves data quality)
- Enforces length limits (prevents DoS)
Parameters:
- input: The raw user input string
- config: Sanitization configuration (nil = use defaults)
Example:
name := core.SanitizeString(userInput, nil)
// Input: "John<script>alert('xss')</script> Doe "
// Output: "John Doe"
func SanitizeURL ¶
SanitizeURL sanitizes URLs to prevent injection attacks.
URL-specific sanitization:
- Removes whitespace
- Blocks javascript: and data: schemes
- Removes null bytes
- Validates basic URL structure
Note: This does NOT validate URL format. Use proper URL validation separately.
Example:
url := core.SanitizeURL(" https://example.com/path ")
// Output: "https://example.com/path"
url := core.SanitizeURL("javascript:alert('xss')")
// Output: "" (dangerous scheme blocked)
func SanitizeUsername ¶
SanitizeUsername sanitizes usernames for safe storage and display.
Username-specific rules:
- Allows alphanumeric, underscore, hyphen, and period
- Removes all other characters
- Converts to lowercase for consistency
- Enforces length limits
Parameters:
- username: The raw username input
- maxLength: Maximum allowed length (0 = use default of 50)
Example:
username := core.SanitizeUsername("John_Doe123!@#", 0)
// Output: "john_doe123"
func SetCustomIDGenerator ¶
func SetCustomIDGenerator(generator IDGeneratorFunc)
SetCustomIDGenerator sets a custom ID generation function.
This automatically switches the ID strategy to IDStrategyCustom. The provided function will be called every time GenerateID() is invoked.
Your custom generator should:
- Return unique IDs (collision-free)
- Be thread-safe if called concurrently
- Generate IDs quickly (called frequently)
Example (using KSUID):
import "github.com/segmentio/ksuid"
core.SetCustomIDGenerator(func() string {
return ksuid.New().String()
})
Example (using database sequences - NOT recommended for distributed systems):
var counter uint64
core.SetCustomIDGenerator(func() string {
return fmt.Sprintf("%d", atomic.AddUint64(&counter, 1))
})
func SetHTTPLogger ¶
func SetHTTPLogger(l HTTPLogger)
SetHTTPLogger sets the logger for HTTP helpers. If set, WriteJSON will log JSON encoding errors instead of silently failing.
Example:
core.SetHTTPLogger(myZapLogger)
func SetIDStrategy ¶
func SetIDStrategy(strategy IDStrategy)
SetIDStrategy sets the global ID generation strategy for the application.
This should be called during application initialization, before any IDs are generated. Changing the strategy after IDs have been generated may cause inconsistent ID formats.
Example:
// Switch to UUID v4 core.SetIDStrategy(core.IDStrategyUUID) // Use ULID (default) core.SetIDStrategy(core.IDStrategyULID)
func SetPluginValue ¶
SetPluginValue is a convenience function to set a plugin value directly in context. Does nothing if plugin data is not initialized.
func StripTags ¶
StripTags removes all HTML tags from a string.
This is a convenience function for quick HTML tag removal.
Example:
text := core.StripTags("<p>Hello <b>World</b></p>")
// Output: "Hello World"
func ValidateEmail ¶
ValidateEmail validates an email address format.
Checks:
- Email is not empty
- Email matches RFC 5322 format (using ozzo-validation)
Returns an error if validation fails. Leading/trailing whitespace is trimmed.
Example:
if err := core.ValidateEmail(email); err != nil {
return fmt.Errorf("invalid email: %w", err)
}
func ValidateMiddleware ¶
func ValidateMiddleware[T interface{ Validate() error }](
handler func(w http.ResponseWriter, r *http.Request, req T),
) http.HandlerFunc
ValidateMiddleware creates a middleware that automatically validates request bodies. T must implement a Validate() error method. The validated request is passed to the handler, eliminating the need for manual validation.
Example usage:
router.POST("/organizations", ValidateMiddleware(p.CreateOrganizationHandler))
func (p *Plugin) CreateOrganizationHandler(
w http.ResponseWriter,
r *http.Request,
req CreateOrganizationRequest, // Already validated!
) {
// Use req directly - validation is guaranteed
}
func ValidatePassword ¶
func ValidatePassword(password string, policy *PasswordPolicyConfig) error
ValidatePassword validates password strength based on a configurable policy.
The validation checks are controlled by the PasswordPolicyConfig:
- MinLength: Minimum character count (default: 8)
- MaxLength: Maximum character count (default: 128, prevents DoS)
- RequireUpper: At least one uppercase letter A-Z
- RequireLower: At least one lowercase letter a-z
- RequireDigit: At least one numeric digit 0-9
- RequireSpecial: At least one special character (!@#$%^&*, etc.)
If policy is nil, DefaultPasswordPolicyConfig is used (8+ chars, mixed case, digit required, special chars optional).
Modern best practices (NIST/OWASP 2024):
- Enforce minimum length (8+ characters)
- Optionally require character diversity
- Check against breached password databases (not implemented here)
- Don't force regular password changes
Parameters:
- password: The plaintext password to validate
- policy: The password policy to enforce (nil = use defaults)
Example:
policy := &core.PasswordPolicyConfig{
MinLength: 12,
RequireSpecial: true,
}
if err := core.ValidatePassword(password, policy); err != nil {
return fmt.Errorf("weak password: %w", err)
}
func ValidatePasswordSimple ¶
ValidatePasswordSimple validates password with basic length requirement only.
This is a simplified validator that only checks minimum length, without requiring character diversity (uppercase, lowercase, digits, symbols).
Use this when:
- Building low-security applications (internal tools, dev environments)
- Users find strict policies too frustrating
- You rely on other security measures (MFA, breach detection, etc.)
For production systems with sensitive data, prefer ValidatePassword with a proper PasswordPolicyConfig.
Parameters:
- password: The plaintext password to validate
- minLength: Minimum character count (0 = use default of 6)
Example:
if err := core.ValidatePasswordSimple(password, 8); err != nil {
return err
}
func VerifyPassword ¶
VerifyPassword verifies a plaintext password against an Argon2id hash.
The function parses the PHC-formatted hash string to extract the algorithm parameters, salt, and expected hash. It then re-hashes the provided password with the same parameters and compares the results using constant-time comparison to prevent timing attacks.
Parameters:
- password: The plaintext password to verify
- encodedHash: The PHC-formatted hash string from HashPassword or database
Returns:
- bool: true if the password matches, false otherwise
- error: validation error if the hash format is invalid or corrupted
Expected PHC format:
$argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>
The function parses:
- Algorithm identifier (must be "argon2id")
- Version (e.g., v=19)
- Parameters: m=memory, t=time, p=parallelism
- Base64-encoded salt
- Base64-encoded hash to verify against
Example:
ok, err := VerifyPassword("my-secret-password", storedHash)
if err != nil {
return err // Hash is malformed
}
if !ok {
return errors.New("invalid password")
}
func WithContextInitialized ¶
WithContextInitialized marks the context as initialized by Aegis. This is used internally to check if AegisContextMiddleware was called.
func WithEnrichedUser ¶
func WithEnrichedUser(ctx context.Context, eu *EnrichedUser) context.Context
WithEnrichedUser adds an enriched user to the context. This is called by AuthMiddleware after creating the EnrichedUser.
func WithPathParamFunc ¶
func WithPathParamFunc(ctx context.Context, fn PathParamFunc) context.Context
WithPathParamFunc adds a path parameter extraction function to the context. This is called by router middleware to inject the router-specific implementation.
func WithPluginData ¶
func WithPluginData(ctx context.Context, pd *PluginData) context.Context
WithPluginData adds a plugin data store to the context. This is called once per request to initialize the plugin data store.
func WithRequestID ¶
WithRequestID adds a request ID to the context for tracing.
func WithRequestMeta ¶
func WithRequestMeta(ctx context.Context, meta *RequestMeta) context.Context
WithRequestMeta adds request metadata to the context. This includes IP address, user agent, method, and path.
func WithSession ¶
WithSession adds a session to the context. This is called by AuthMiddleware along with WithUser.
func WithUser ¶
WithUser adds a user to the context. This is typically called by AuthMiddleware after validating a session.
func WriteJSON ¶
func WriteJSON(w http.ResponseWriter, statusCode int, data any)
WriteJSON writes a JSON response with the given status code and data.
Automatically sets Content-Type header to application/json. If JSON encoding fails and an HTTPLogger is configured (via SetHTTPLogger), the error is logged.
Example:
core.WriteJSON(w, 200, map[string]string{"status": "ok"})
func WriteJSONError ¶
func WriteJSONError(w http.ResponseWriter, statusCode int, message string)
WriteJSONError writes a JSON error response with the given status code and message.
This is a convenience wrapper around WriteJSON for error responses.
Example:
core.WriteJSONError(w, 400, "Invalid request")
// Output: {"error": "Invalid request"}
Types ¶
type AccountModel ¶
type AccountModel interface {
// GetID returns the unique identifier for this account
GetID() string
// SetID assigns a unique identifier to this account
SetID(string)
// GetUserID returns the ID of the user this account belongs to
GetUserID() string
// SetUserID assigns the owning user's ID
SetUserID(string)
// GetProvider returns the authentication provider name (e.g., "credentials", "google")
GetProvider() string
// SetProvider assigns the authentication provider name
SetProvider(string)
// GetPasswordHash returns the hashed password (for credential-based accounts)
GetPasswordHash() string
// SetPasswordHash assigns the hashed password
SetPasswordHash(string)
// SetCreatedAt assigns the creation timestamp
SetCreatedAt(time.Time)
// SetUpdatedAt assigns the last modification timestamp
SetUpdatedAt(time.Time)
// GetExpiresAt returns when OAuth tokens expire (OAuth accounts only)
GetExpiresAt() time.Time
// SetExpiresAt assigns the OAuth token expiration time
SetExpiresAt(time.Time)
// GetAccessToken returns the OAuth access token (OAuth accounts only)
GetAccessToken() string
// SetAccessToken assigns the OAuth access token
SetAccessToken(string)
// GetRefreshToken returns the OAuth refresh token (OAuth accounts only)
GetRefreshToken() string
// SetRefreshToken assigns the OAuth refresh token
SetRefreshToken(string)
// GetProviderAccountID returns the provider-specific user identifier
GetProviderAccountID() string
// SetProviderAccountID assigns the provider-specific user identifier
SetProviderAccountID(string)
}
AccountModel defines the required methods for an account model implementation. Accounts link users to authentication providers (credentials, OAuth, etc.).
type AccountService ¶
type AccountService struct {
// contains filtered or unexported fields
}
AccountService manages authentication accounts linked to users.
An "account" represents a specific authentication method for a user:
- Credential account (email/password)
- OAuth account (Google, GitHub, etc.)
- Other provider accounts (SAML, LDAP, etc.)
A single user can have multiple accounts (one per provider), enabling multi-provider authentication (login with email OR Google, for example).
Key responsibilities:
- Account creation and retrieval
- Password updates with session invalidation
- Provider-specific account management
- Audit logging of account changes
func NewAccountService ¶
func NewAccountService(accountStore auth.AccountStore, sessionStore auth.SessionStore, hashConfig *PasswordHasherConfig, authConfig *AuthConfig, auditLogger AuditLogger) *AccountService
NewAccountService creates a new account service with the specified dependencies.
func (*AccountService) CreateAccount ¶
CreateAccount creates a new account
func (*AccountService) DeleteAccount ¶
func (s *AccountService) DeleteAccount(ctx context.Context, id string) error
DeleteAccount deletes a user account by its ID.
func (*AccountService) GetAccountByID ¶
GetAccountByID retrieves an account by ID
func (*AccountService) GetAccountsByUserID ¶
func (s *AccountService) GetAccountsByUserID(ctx context.Context, userID string) ([]auth.Account, error)
GetAccountsByUserID retrieves all accounts for a user
func (*AccountService) GetPasswordAccount ¶
func (s *AccountService) GetPasswordAccount(ctx context.Context, userID string) (auth.Account, error)
GetPasswordAccount retrieves the password account for a user
func (*AccountService) UpdatePassword ¶
func (s *AccountService) UpdatePassword(ctx context.Context, userID, newPassword string) error
UpdatePassword changes a user's password and invalidates existing sessions.
Security considerations:
- The new password is hashed with Argon2id before storage
- All existing sessions are invalidated (user must re-login)
- The operation is audited for security monitoring
Flow:
- Find the user's password account (provider="credentials")
- Hash the new password
- Update the account with the new hash
- Delete all sessions to force re-authentication
- Log the password change event
Parameters:
- ctx: Request context
- userID: ID of the user whose password is being changed
- newPassword: Plaintext new password to hash and store
Returns an error if:
- User has no password account (OAuth-only user)
- Password hashing fails
- Database update fails
Session deletion errors are logged but don't fail the operation.
func (*AccountService) VerifyPassword ¶
VerifyPassword verifies a user's password
type AegisContext ¶
type AegisContext struct {
// contains filtered or unexported fields
}
AegisContext is a builder for creating Aegis-enriched contexts. Useful for testing and programmatic context creation.
func NewAegisContext ¶
func NewAegisContext(ctx context.Context) *AegisContext
NewAegisContext creates a new context builder from an existing context.
func (*AegisContext) Context ¶
func (ac *AegisContext) Context() context.Context
Context returns the built context.
func (*AegisContext) WithExtension ¶
func (ac *AegisContext) WithExtension(key string, value any) *AegisContext
WithExtension adds a user extension to the context. Requires WithUser to be called first.
func (*AegisContext) WithPluginData ¶
func (ac *AegisContext) WithPluginData() *AegisContext
WithPluginData adds plugin data to the context.
func (*AegisContext) WithRequestID ¶
func (ac *AegisContext) WithRequestID(id string) *AegisContext
WithRequestID adds a request ID to the context.
func (*AegisContext) WithRequestMeta ¶
func (ac *AegisContext) WithRequestMeta(meta *RequestMeta) *AegisContext
WithRequestMeta adds request metadata to the context.
func (*AegisContext) WithSession ¶
func (ac *AegisContext) WithSession(session *auth.Session) *AegisContext
WithSession adds a session to the context.
func (*AegisContext) WithUser ¶
func (ac *AegisContext) WithUser(user *auth.User) *AegisContext
WithUser adds a user to the context.
type AuditEvent ¶
type AuditEvent struct {
// ID is a unique identifier for this event
ID string `json:"id"`
// EventType categorizes what happened
EventType AuditEventType `json:"event_type"`
// UserID identifies who performed the action (empty if unauthenticated)
UserID string `json:"user_id,omitempty"`
// IPAddress of the client that triggered this event
IPAddress string `json:"ip_address,omitempty"`
// UserAgent of the client that triggered this event
UserAgent string `json:"user_agent,omitempty"`
// Resource identifies what was acted upon (e.g., "user:123", "session:abc")
Resource string `json:"resource,omitempty"`
// Action describes what was done (e.g., "create", "update", "delete")
Action string `json:"action,omitempty"`
// Details contains additional context specific to this event type
Details map[string]any `json:"details,omitempty"`
// Timestamp of when the event occurred
Timestamp time.Time `json:"timestamp"`
// Success indicates if the action succeeded
Success bool `json:"success"`
// Error contains the error message if Success is false
Error string `json:"error,omitempty"`
}
AuditEvent represents a structured security audit log entry.
Audit logs enable:
- Security monitoring and threat detection
- Compliance reporting (GDPR, SOC 2, HIPAA, etc.)
- Forensic investigation after incidents
- User activity tracking
Events should be written to durable storage (database, log aggregator) for retention and analysis.
type AuditEventType ¶
type AuditEventType string
AuditEventType categorizes different types of security and authentication events. These types enable filtering, alerting, and compliance reporting.
const ( // AuditEventLoginSuccess indicates a successful user authentication AuditEventLoginSuccess AuditEventType = "login_success" // AuditEventLoginFailed indicates a failed authentication attempt // (wrong password, non-existent user, account locked, etc.) AuditEventLoginFailed AuditEventType = "login_failed" // AuditEventLogout indicates an explicit user logout AuditEventLogout AuditEventType = "logout" // AuditEventSessionRefresh indicates a session was refreshed using a refresh token AuditEventSessionRefresh AuditEventType = "session_refresh" // AuditEventSessionExpired indicates a session expired due to timeout AuditEventSessionExpired AuditEventType = "session_expired" // AuditEventUserCreated indicates a new user account was created AuditEventUserCreated AuditEventType = "user_created" // AuditEventUserUpdated indicates user data was modified AuditEventUserUpdated AuditEventType = "user_updated" // AuditEventUserDeleted indicates a user account was deleted AuditEventUserDeleted AuditEventType = "user_deleted" // AuditEventEmailChanged indicates a user's email address was changed AuditEventEmailChanged AuditEventType = "email_changed" // AuditEventPasswordChanged indicates a user changed their password AuditEventPasswordChanged AuditEventType = "password_changed" // AuditEventPasswordReset indicates a password was reset via recovery flow AuditEventPasswordReset AuditEventType = "password_reset" // AuditEventRateLimitHit indicates a client exceeded rate limits AuditEventRateLimitHit AuditEventType = "rate_limit_hit" // AuditEventAccountLocked indicates an account was locked due to failed attempts AuditEventAccountLocked AuditEventType = "account_locked" // AuditEventSuspiciousActivity indicates anomalous behavior was detected AuditEventSuspiciousActivity AuditEventType = "suspicious_activity" )
type AuditLogger ¶
type AuditLogger interface {
// LogEvent records a detailed audit event.
// Should not block - consider async/buffered implementations for high throughput.
LogEvent(ctx context.Context, event *AuditEvent) error
// LogAuthEvent is a convenience method for common authentication events.
// Creates and logs an AuditEvent with authentication-specific fields.
// IP address and user agent are automatically extracted from the request
// context (populated by AegisContextMiddleware).
LogAuthEvent(ctx context.Context, eventType AuditEventType, userID string, success bool, details map[string]any) error
}
AuditLogger defines the interface for audit event logging. Implementations can write to databases, files, log aggregators (Splunk, Elasticsearch), or SIEM systems.
type AuthConfig ¶
type AuthConfig struct {
// EnableEmailPassword controls whether email/password authentication is available.
// When false, users cannot signup or login with credentials (OAuth/SSO only).
EnableEmailPassword bool
// PasswordPolicy defines password strength requirements for signup/change.
// If nil, uses DefaultPasswordPolicyConfig (8+ chars, mixed case, digit required).
PasswordPolicy *PasswordPolicyConfig
// InvalidateSessionsOnPasswordChange, when true, logs users out from all
// devices when their password changes. This is a security best practice that
// prevents attackers from maintaining access after a password is reset.
// Recommended: true
InvalidateSessionsOnPasswordChange bool
// UserFields controls which plugin extension fields are included in user
// API responses. If nil, all extension fields are included.
// Use this to limit what data is exposed in user objects.
UserFields *UserFieldsConfig
}
AuthConfig defines core authentication system configuration. This is the primary configuration struct passed to NewAuthService.
func DefaultAuthConfig ¶
func DefaultAuthConfig() *AuthConfig
DefaultAuthConfig returns default authentication configuration
type AuthError ¶
type AuthError struct {
// Code is a machine-readable error code (e.g., "INVALID_CREDENTIALS")
Code string
// Message is a human-readable error description
Message string
// Cause is the underlying error that triggered this auth error (optional)
Cause error
}
AuthError represents an authentication-specific error with additional context. It wraps an optional cause error and includes a machine-readable code for API responses.
func NewAuthError ¶
NewAuthError creates a new AuthError with the given code and message.
func NewAuthErrorWithCause ¶
NewAuthErrorWithCause creates a new AuthError wrapping an underlying cause.
type AuthService ¶
type AuthService struct {
// Sub-services for specialized operations
User *UserService
Account *AccountService
Session *SessionService
Verification *VerificationService
EmailPassword *EmailPasswordHandlers
// contains filtered or unexported fields
}
AuthService is the main orchestrator for authentication operations. It coordinates specialized sub-services and provides centralized access to authentication functionality throughout the application.
AuthService manages:
- Password hashing configuration
- Audit logging
- Login attempt tracking for account lockout
- Authentication policy configuration
It provides four sub-services that handle specific domains:
- User: User CRUD operations
- Account: Authentication account management
- Session: Session lifecycle and caching
- Verification: Token-based verification flows
AuthService should be initialized once at application startup and shared across HTTP handlers and middleware.
func NewAuthService ¶
func NewAuthService(authConfig *AuthConfig, authConn *auth.Auth, hashConfig *PasswordHasherConfig, auditLogger AuditLogger, loginAttemptTracker *LoginAttemptTracker) *AuthService
NewAuthService creates a new AuthService with all sub-services initialized.
Parameters:
- authConfig: Authentication policy configuration (session duration, password policy, etc.). If nil, defaults are used.
- authConn: Connection to the auth storage layer providing access to stores.
- hashConfig: Argon2id password hashing parameters. If nil, secure OWASP- recommended defaults are used.
- auditLogger: Interface for logging security events. If nil, a no-op logger is used (events are silently discarded).
- loginAttemptTracker: Tracks failed login attempts for account lockout. Can be nil if brute force protection is not needed.
The function ensures all nil inputs are replaced with safe defaults, so it will never return a partially-configured service.
func (*AuthService) GetAuthConfig ¶
func (as *AuthService) GetAuthConfig() *AuthConfig
GetAuthConfig returns the authentication configuration used by this service. This includes session settings, password policy, and user field filtering.
func (*AuthService) GetUserFieldsConfig ¶
func (as *AuthService) GetUserFieldsConfig() *UserFieldsConfig
GetUserFieldsConfig returns the user fields configuration which controls which user fields are included or excluded in API responses.
Returns nil if not configured, meaning all fields are included by default.
type CookieManager ¶
type CookieManager struct {
// contains filtered or unexported fields
}
CookieManager provides centralized cookie management for Aegis sessions.
This manager encapsulates cookie security best practices:
- HTTPOnly: Prevents JavaScript access (XSS protection)
- Secure: Requires HTTPS in production (prevents MITM attacks)
- SameSite: Prevents CSRF attacks (Lax, Strict, or None)
- Configurable domain: Supports subdomain sharing
- Configurable path: Limits cookie scope
The CookieManager is created by SessionService and uses settings from SessionConfig.CookieSettings. All session cookies are managed through this abstraction for consistency.
Cookie Security Best Practices:
- Always enable HTTPOnly (prevents XSS from stealing cookies)
- Always enable Secure in production (requires HTTPS)
- Use SameSite=Lax for general APIs, Strict for sensitive operations
- Use SameSite=None only when needed for cross-site requests (requires Secure=true)
Example:
cm := core.NewCookieManager(sessionConfig) cm.SetSessionCookie(w, sessionToken) // Sets with configured security token, err := cm.GetSessionCookie(r) // Reads session cookie cm.ClearSessionCookie(w) // Deletes the session cookie
func NewCookieManager ¶
func NewCookieManager(cfg *SessionConfig) *CookieManager
NewCookieManager creates a new CookieManager with the given configuration.
If cfg is nil, uses DefaultSessionConfig() with secure defaults.
The CookieManager will use the settings from cfg.CookieSettings for all cookie operations (HTTPOnly, Secure, SameSite, Domain, Path, Name).
func (*CookieManager) ClearSessionCookie ¶
func (cm *CookieManager) ClearSessionCookie(w http.ResponseWriter)
ClearSessionCookie deletes the session cookie by setting MaxAge to -1.
This is called during logout to invalidate the client-side session. The server-side session is deleted separately via SessionService.DeleteSession.
Note: Even after clearing the cookie, the session token remains valid in the database until DeleteSession is called or the session expires naturally.
Example:
sessionService.DeleteSession(ctx, sessionID) // Server-side cleanup cookieManager.ClearSessionCookie(w) // Client-side cleanup
func (*CookieManager) GetConfig ¶
func (cm *CookieManager) GetConfig() *SessionConfig
GetConfig returns the underlying SessionConfig used by this CookieManager. Useful for inspecting current cookie settings.
func (*CookieManager) GetCookie ¶
GetCookie retrieves a cookie value by name from the HTTP request.
Returns an AuthError if:
- Cookie doesn't exist (AuthErrorCodeUnauthorized)
- Reading the cookie fails (AuthErrorCodeInternal)
Example:
csrfToken, err := cm.GetCookie(r, "csrf_token")
if err != nil {
// Cookie missing or error
}
func (*CookieManager) GetSessionCookie ¶
func (cm *CookieManager) GetSessionCookie(r *http.Request) (string, error)
GetSessionCookie retrieves the session token from the configured session cookie.
This is the primary method used by AuthMiddleware to extract the session token for authentication. The cookie name is determined by GetSessionCookieName().
Returns an AuthError if the cookie is missing or cannot be read.
Example:
token, err := cm.GetSessionCookie(r)
if err != nil {
// User not authenticated via cookie
}
func (*CookieManager) GetSessionCookieName ¶
func (cm *CookieManager) GetSessionCookieName() string
GetSessionCookieName returns the configured session cookie name. Returns DefaultCookieName ("aegis_session") if not explicitly configured.
func (*CookieManager) SetCookie ¶
func (cm *CookieManager) SetCookie(w http.ResponseWriter, name, value string, maxAge time.Duration)
SetCookie sets a cookie with the configured security defaults.
This is a convenience method that applies CookieSettings from the config:
- Domain from config.CookieSettings.Domain
- Secure from config.CookieSettings.Secure
- HTTPOnly from config.CookieSettings.HTTPOnly
- SameSite from config.CookieSettings.SameSite
- Path is always "/" (DefaultCookiePath)
Parameters:
- name: Cookie name
- value: Cookie value
- maxAge: Cookie lifetime (0 for session cookies, negative to delete)
Example:
cm.SetCookie(w, "custom_data", "value", 24*time.Hour)
func (*CookieManager) SetCustomCookie ¶
func (cm *CookieManager) SetCustomCookie(w http.ResponseWriter, opts CookieOptions)
SetCustomCookie sets a custom cookie with fine-grained control over all options.
This allows overriding specific cookie properties while still benefiting from config defaults for unspecified fields:
- SameSite: If zero, uses config.CookieSettings.SameSite
- Domain: If empty, uses config.CookieSettings.Domain
Use this for plugin-specific cookies (CSRF tokens, OAuth state, etc.) that need different settings than the main session cookie.
Example:
cm.SetCustomCookie(w, core.CookieOptions{
Name: "csrf_token",
Value: csrfToken,
Path: "/",
MaxAge: 3600, // 1 hour
HTTPOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
func (*CookieManager) SetSessionCookie ¶
func (cm *CookieManager) SetSessionCookie(w http.ResponseWriter, token string)
SetSessionCookie sets the session cookie with configured security settings.
This is the primary method for creating session cookies after successful login. The cookie expires according to config.SessionExpiry.
Security settings applied:
- HTTPOnly: true (prevents XSS)
- Secure: from config (true in production)
- SameSite: from config (Lax/Strict for CSRF protection)
- Domain: from config (supports subdomain sharing)
Parameters:
- token: The session token (random value from CreateSession)
Example:
session, _ := sessionService.CreateSession(ctx, user) cookieManager.SetSessionCookie(w, session.Token)
type CookieOptions ¶
type CookieOptions struct {
// Name is the cookie name
Name string
// Value is the cookie value (should not contain sensitive data unless encrypted)
Value string
// Path restricts the cookie to specific URL paths (default: "/")
Path string
// MaxAge is the cookie lifetime in seconds
// Positive: Persistent cookie (survives browser restart)
// Zero: Session cookie (deleted when browser closes)
// Negative: Delete cookie immediately
MaxAge int
// Domain restricts the cookie to a specific domain or subdomain
// Empty: Current domain only
// ".example.com": All subdomains of example.com
Domain string
// Secure requires HTTPS for cookie transmission
// Always true in production, can be false in local development
Secure bool
// HTTPOnly prevents JavaScript access to the cookie
// Always true for session cookies to prevent XSS attacks
HTTPOnly bool
// SameSite controls cross-site cookie behavior (CSRF protection)
// SameSiteLaxMode: Cookies sent with top-level navigation (default, good balance)
// SameSiteStrictMode: Cookies never sent cross-site (maximum security)
// SameSiteNoneMode: Cookies always sent (requires Secure=true, needed for OAuth flows)
SameSite http.SameSite
}
CookieOptions defines options for setting a custom cookie. Used with SetCustomCookie for fine-grained control over cookie properties.
type CookieSettings ¶
type CookieSettings struct {
// Name is the cookie name (default: "aegis_session")
Name string
// Domain controls which domains can access the cookie.
// Empty: Current domain only
// ".example.com": All subdomains of example.com
Domain string
// Secure requires HTTPS for cookie transmission.
// Always true in production. Can be false for local development.
Secure bool
// HTTPOnly prevents JavaScript from accessing the cookie.
// Always true for session cookies (XSS protection).
HTTPOnly bool
// SameSite controls cross-site cookie behavior (CSRF protection).
// Options:
// - "Strict": Cookie never sent in cross-site requests
// - "Lax": Cookie sent on top-level navigation (default, recommended)
// - "None": Cookie always sent (requires Secure=true)
SameSite string
}
CookieSettings defines HTTP cookie configuration for session tokens.
Security best practices:
- Always set HTTPOnly=true (prevents JavaScript access)
- Set Secure=true in production (HTTPS only)
- Use SameSite="Lax" or "Strict" (CSRF protection)
- Set Domain to your domain for subdomain sharing
type EmailPasswordHandlers ¶
type EmailPasswordHandlers struct {
// contains filtered or unexported fields
}
EmailPasswordHandlers provides HTTP handlers and programmatic functions for traditional email+password authentication.
This handler set implements the classic username/password authentication flow:
- Registration: Create a new user with email+password
- Login: Authenticate existing user with credentials
HTTP handlers are private (lowercase) and automatically mounted. For programmatic use without HTTP, use the public methods:
- Login(ctx, email, password)
- Register(ctx, name, email, password)
IP address and user agent are automatically extracted from the request context (populated by AegisContextMiddleware). For non-HTTP usage, populate the context with WithRequestMeta.
func NewEmailPasswordHandlers ¶
func NewEmailPasswordHandlers(authService *AuthService) *EmailPasswordHandlers
NewEmailPasswordHandlers creates a new set of email+password authentication handlers.
func (*EmailPasswordHandlers) Login ¶
func (h *EmailPasswordHandlers) Login(ctx context.Context, email, password string) (*LoginResult, error)
Login authenticates a user with email and password programmatically. IP address and user agent are automatically extracted from the request context.
func (*EmailPasswordHandlers) Register ¶
func (h *EmailPasswordHandlers) Register(ctx context.Context, name, email, password string) (*RegisterResult, error)
Register registers a new user with email and password programmatically. IP address and user agent are automatically extracted from the request context.
type EnrichedUser ¶
type EnrichedUser struct {
*auth.User
// Extensions holds additional fields from plugins.
// Keys are simple field names: "role", "verified", "organizations", etc.
// These are flattened into the JSON response as top-level fields.
Extensions map[string]any `json:"-"` // Excluded from default marshal, handled in MarshalJSON
// contains filtered or unexported fields
}
EnrichedUser wraps the core auth.User with plugin-specific extensions.
This is a key extensibility mechanism in Aegis that allows plugins to augment the base user model with additional fields without modifying the core schema. Plugin data is stored in the Extensions map and automatically merged into JSON responses.
Common use cases:
- Admin plugin adds: "role", "permissions"
- Organizations plugin adds: "organizations", "currentOrg"
- JWT plugin adds: "claims", "tokenExp"
- Email verification plugin adds: "emailVerified"
Extension keys should be simple field names (not nested paths). The MarshalJSON implementation flattens extensions as top-level fields in API responses.
Example plugin usage:
enriched := core.GetEnrichedUser(ctx)
enriched.Set("role", "admin")
enriched.Set("emailVerified", true)
enriched.Set("organizations", []string{"org1", "org2"})
Example API response:
{
"id": "01HXYZ...",
"email": "[email protected]",
"name": "John Doe",
"role": "admin", // From extension
"emailVerified": true, // From extension
"organizations": ["org1", "org2"] // From extension
}
Thread safety: All methods are safe for concurrent use via internal mutex.
func GetEnrichedUser ¶
func GetEnrichedUser(ctx context.Context) *EnrichedUser
GetEnrichedUser extracts the enriched user from context. This includes all plugin extensions (admin role, jwt claims, org memberships, etc.) Returns nil if no authenticated user or enriched user not set.
func MustGetEnrichedUser ¶
func MustGetEnrichedUser(ctx context.Context) *EnrichedUser
MustGetEnrichedUser extracts the enriched user, panicking if not found. Use only in handlers where authentication is guaranteed.
func NewEnrichedUser ¶
func NewEnrichedUser(user *auth.User) *EnrichedUser
NewEnrichedUser creates an EnrichedUser wrapping a core User. The Extensions map is initialized empty, ready for plugins to populate.
func (*EnrichedUser) Get ¶
func (eu *EnrichedUser) Get(key string) any
Get retrieves an extension value by key. Returns nil if the key doesn't exist.
For type-safe access, prefer the typed getters (GetString, GetBool, etc.).
func (*EnrichedUser) GetBool ¶
func (eu *EnrichedUser) GetBool(key string) bool
GetBool retrieves a bool extension value. Returns false if the key doesn't exist or value is not a bool.
func (*EnrichedUser) GetMap ¶
func (eu *EnrichedUser) GetMap(key string) map[string]any
GetMap retrieves a map extension value. Returns nil if the key doesn't exist or value is not a map.
func (*EnrichedUser) GetString ¶
func (eu *EnrichedUser) GetString(key string) string
GetString retrieves a string extension value. Returns empty string if the key doesn't exist or value is not a string.
func (*EnrichedUser) GetStringSlice ¶
func (eu *EnrichedUser) GetStringSlice(key string) []string
GetStringSlice retrieves a string slice extension value. Returns nil if the key doesn't exist or value is not a []string.
func (*EnrichedUser) Has ¶
func (eu *EnrichedUser) Has(key string) bool
Has checks if an extension key exists. Returns true even if the value is nil.
func (*EnrichedUser) Keys ¶
func (eu *EnrichedUser) Keys() []string
Keys returns all extension keys currently set. Useful for debugging or iterating over all extensions.
func (*EnrichedUser) MarshalJSON ¶
func (eu *EnrichedUser) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler for API responses. Extensions are flattened as top-level fields in the JSON output.
func (*EnrichedUser) Set ¶
func (eu *EnrichedUser) Set(key string, value any)
Set adds or updates an extension field.
This is typically called by plugins during request processing to add their data to the user context. Key should be a simple field name that will become a top-level field in JSON responses.
Thread-safe for concurrent plugin access.
func (*EnrichedUser) ToAPIResponse ¶
func (eu *EnrichedUser) ToAPIResponse() map[string]any
ToAPIResponse returns a map suitable for JSON API responses. Extensions are flattened as top-level fields.
func (*EnrichedUser) ToAPIResponseFiltered ¶
func (eu *EnrichedUser) ToAPIResponseFiltered(config *UserFieldsConfig) map[string]any
ToAPIResponseFiltered returns a map suitable for JSON API responses, optionally filtering extension fields based on the provided config. If config is nil, all fields are included.
type HTTPLogger ¶
HTTPLogger is an optional interface for logging HTTP helper errors. This is a subset of structured logging interfaces (zap, logrus, slog).
type IDGeneratorFunc ¶
type IDGeneratorFunc func() string
IDGeneratorFunc is a function type for custom ID generation. Implement this to use your own ID generation algorithm (KSUIDs, nanoid, Snowflake, etc.).
type IDStrategy ¶
type IDStrategy string
IDStrategy defines the algorithm used for generating unique identifiers.
Aegis supports multiple ID generation strategies to accommodate different use cases and preferences.
const ( // IDStrategyULID uses ULID (Universally Unique Lexicographically Sortable Identifier). // This is the DEFAULT strategy. // // Benefits: // - Sortable: IDs are ordered by creation time // - Compact: 26 characters (vs 36 for UUID) // - No configuration needed: Works immediately // - Collision resistant: 80 bits of randomness // - Database friendly: Efficient indexing due to sortability // // Format: 01ARZ3NDEKTSV4RRFFQ69G5FAV (26 characters) // Structure: 10-byte timestamp + 16-byte randomness // // Best for: Most use cases, especially when you need sortable IDs IDStrategyULID IDStrategy = "ulid" // IDStrategyUUID uses UUID v4 (random UUIDs). // // Benefits: // - Standard format: Widely recognized and supported // - Maximum randomness: 122 bits of entropy // - Collision resistant: Extremely low probability of collisions // // Drawbacks: // - Not sortable: IDs are random, not time-ordered // - Longer: 36 characters with hyphens // - Database indexing: Less efficient than ULIDs // // Format: 550e8400-e29b-41d4-a716-446655440000 (36 characters) // // Best for: When you need standard UUID format or maximum randomness IDStrategyUUID IDStrategy = "uuid" // IDStrategyCustom uses a user-provided custom ID generation function. // // Use this when you need: // - KSUID (K-Sortable Unique Identifier) // - Snowflake IDs (Twitter's distributed ID generation) // - Nanoid (shorter, URL-safe IDs) // - Database-generated IDs (SERIAL, AUTO_INCREMENT) // - Custom formatting or business logic // // Set the custom generator with: // core.SetCustomIDGenerator(func() string { return myIDGenerator() }) // // Best for: Specialized requirements or existing ID systems IDStrategyCustom IDStrategy = "custom" )
func GetIDStrategy ¶
func GetIDStrategy() IDStrategy
GetIDStrategy returns the currently active ID generation strategy.
Useful for logging or debugging to verify which strategy is in use.
type KeyManager ¶
type KeyManager interface {
// Get retrieves a value by key
// Returns error if key doesn't exist or retrieval fails
Get(ctx context.Context, key string) ([]byte, error)
// Set stores a value with optional expiry
// expiry=0 means no expiration (permanent storage)
Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
// Delete removes a value by key
Delete(ctx context.Context, key string) error
}
KeyManager defines a general-purpose key-value storage interface.
This interface is used by plugins for storing temporary data:
- OAuth state tokens (OAuth plugin)
- CSRF tokens (CSRF protection)
- Email verification codes (EmailOTP plugin)
- SMS verification codes (SMS plugin)
- JWT refresh token blacklists (JWT plugin)
Implementations:
- StaticKeyManager: In-memory storage (development/testing)
- RedisKeyManager: Redis-backed storage (production)
Unlike SessionService which uses Redis for session caching, KeyManager is a general-purpose abstraction that plugins can use for any temporary data.
Example (OAuth state storage):
keyManager.Set(ctx, "oauth:state:"+state, []byte(redirectURL), 10*time.Minute) redirectURL, _ := keyManager.Get(ctx, "oauth:state:"+state) keyManager.Delete(ctx, "oauth:state:"+state)
type LoggerAuditLogger ¶
type LoggerAuditLogger struct {
// contains filtered or unexported fields
}
LoggerAuditLogger implements AuditLogger using a structured logger interface. This adapter allows using any logger (zap, logrus, slog) that implements the Info/Error/Debug methods.
func NewLoggerAuditLogger ¶
func NewLoggerAuditLogger(logger interface {
Info(msg string, keysAndValues ...any)
Error(msg string, keysAndValues ...any)
Debug(msg string, keysAndValues ...any)
}) *LoggerAuditLogger
NewLoggerAuditLogger creates an audit logger that writes to a structured logger.
func (*LoggerAuditLogger) LogAuthEvent ¶
func (l *LoggerAuditLogger) LogAuthEvent(ctx context.Context, eventType AuditEventType, userID string, success bool, details map[string]any) error
LogAuthEvent implements AuditLogger. IP address and user agent are extracted from the request context.
func (*LoggerAuditLogger) LogEvent ¶
func (l *LoggerAuditLogger) LogEvent(_ context.Context, event *AuditEvent) error
LogEvent implements AuditLogger.
type LoginAttemptConfig ¶
type LoginAttemptConfig struct {
// MaxAttempts is the threshold before triggering account lockout.
// Example: 5 means lock after the 5th failed attempt.
MaxAttempts int
// LockoutDuration is how long the account remains locked.
// After this duration, the user can attempt to login again.
LockoutDuration time.Duration
// AttemptWindow is the time window for counting failed attempts.
// Attempts older than this window don't count toward the limit.
AttemptWindow time.Duration
}
LoginAttemptConfig configures login attempt tracking behavior.
func DefaultLoginAttemptConfig ¶
func DefaultLoginAttemptConfig() *LoginAttemptConfig
DefaultLoginAttemptConfig returns sensible defaults
type LoginAttemptTracker ¶
type LoginAttemptTracker struct {
// contains filtered or unexported fields
}
LoginAttemptTracker tracks failed login attempts for account lockout protection.
This prevents brute force attacks by temporarily locking accounts after too many failed login attempts. Like RateLimiter, it supports both Redis (distributed) and in-memory (single-instance) backends.
func NewLoginAttemptTracker ¶
func NewLoginAttemptTracker(config *LoginAttemptConfig, redisClient *redis.Client) *LoginAttemptTracker
NewLoginAttemptTracker creates a new login attempt tracker
func (*LoginAttemptTracker) ClearAttempts ¶
func (lat *LoginAttemptTracker) ClearAttempts(ctx context.Context, identifier string) error
ClearAttempts clears failed attempts for an identifier (on successful login)
func (*LoginAttemptTracker) IsLockedOut ¶
func (lat *LoginAttemptTracker) IsLockedOut(ctx context.Context, identifier string) (bool, time.Duration, error)
IsLockedOut checks if an identifier is locked out
func (*LoginAttemptTracker) RecordFailedAttempt ¶
func (lat *LoginAttemptTracker) RecordFailedAttempt(ctx context.Context, identifier string) (int, bool, error)
RecordFailedAttempt records a failed login attempt
func (*LoginAttemptTracker) Stop ¶
func (lat *LoginAttemptTracker) Stop()
Stop stops the tracker cleanup goroutine
type LoginRequest ¶
LoginRequest represents the JSON payload for email+password login.
type LoginResult ¶
type LoginResult struct {
// User is the authenticated user
User auth.User
// Session is the newly created session
Session *auth.Session
// Token is the session token
Token string
}
LoginResult contains the result of an email+password login.
type NoOpAuditLogger ¶
type NoOpAuditLogger struct{}
NoOpAuditLogger is a no-op implementation that discards all events. Useful for testing or when audit logging is not required.
func (*NoOpAuditLogger) LogAuthEvent ¶
func (n *NoOpAuditLogger) LogAuthEvent(_ context.Context, _ AuditEventType, _ string, _ bool, _ map[string]any) error
LogAuthEvent implements AuditLogger.
func (*NoOpAuditLogger) LogEvent ¶
func (n *NoOpAuditLogger) LogEvent(_ context.Context, _ *AuditEvent) error
LogEvent implements AuditLogger.
type PaginationParams ¶
type PaginationParams struct {
// Page is the 1-based page number (default: 1)
Page int
// Limit is the number of items per page (default: 20, max: 100)
Limit int
// Offset is the calculated skip offset for database queries
// Automatically calculated as (Page-1) * Limit
Offset int
}
PaginationParams holds parsed and validated pagination parameters. Used with ParsePagination to extract pagination from query strings.
func ParsePagination ¶
func ParsePagination(r *http.Request) PaginationParams
ParsePagination extracts and validates pagination parameters from request query string.
Query parameters:
- page: Page number (1-based, default: 1)
- limit: Items per page (default: 20, max: 100)
Invalid values are replaced with defaults:
- page < 1 becomes 1
- limit < 1 or limit > 100 becomes 20
Example:
params := core.ParsePagination(r) // ?page=2&limit=50 users, _ := userStore.List(ctx, params.Offset, params.Limit)
type PasswordHasherConfig ¶
type PasswordHasherConfig struct {
// Argon2Time is the number of iterations (time cost)
Argon2Time uint32
// Argon2Memory is the memory cost in KiB (e.g., 65536 = 64MB)
Argon2Memory uint32
// Argon2Threads is the degree of parallelism (typically 4)
Argon2Threads uint8
// Argon2KeyLength is the derived key length in bytes (typically 32)
Argon2KeyLength uint32
}
PasswordHasherConfig defines Argon2id parameters for password hashing.
Argon2id is memory-hard and resistant to GPU/ASIC attacks. Higher values increase security at the cost of CPU/memory usage during login.
OWASP 2024 recommendations:
- Time: 1-3 iterations
- Memory: 64MB-256MB (64*1024 - 256*1024 KiB)
- Threads: Match CPU cores (typically 4)
- KeyLength: 32 bytes (256 bits)
For high-security applications, increase Memory to 256MB+ and Time to 3+. For resource-constrained environments, keep defaults but monitor load.
func DefaultPasswordHasherConfig ¶
func DefaultPasswordHasherConfig() *PasswordHasherConfig
DefaultPasswordHasherConfig returns default password hashing configuration.
type PasswordPolicyConfig ¶
type PasswordPolicyConfig struct {
// MinLength is minimum password length (default: 8, NIST minimum)
MinLength int
// RequireUpper requires at least one uppercase letter (default: true)
RequireUpper bool
// RequireLower requires at least one lowercase letter (default: true)
RequireLower bool
// RequireDigit requires at least one numeric digit (default: true)
RequireDigit bool
// RequireSpecial requires at least one special character (default: false)
// Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?
RequireSpecial bool
// MaxLength caps password length to prevent DoS (default: 128, 0 = unlimited)
// Very long passwords can cause excessive CPU usage during hashing.
MaxLength int
}
PasswordPolicyConfig defines password validation rules.
Modern password policy recommendations (NIST/OWASP 2024):
- Require minimum length (8+ characters)
- Optionally require character diversity (mixed case, digits, symbols)
- Don't require forced expiration or rotation
- Check against breached password databases
Note: Overly strict policies can lead to weaker passwords (users write them down, use predictable patterns, etc.). Balance security with usability.
func DefaultPasswordPolicyConfig ¶
func DefaultPasswordPolicyConfig() *PasswordPolicyConfig
DefaultPasswordPolicyConfig returns default password policy configuration
type PathParamFunc ¶
PathParamFunc is a function type for extracting path parameters from requests. This allows different routers to provide their own implementation.
type PluginData ¶
type PluginData struct {
// contains filtered or unexported fields
}
PluginData is a thread-safe store for plugin-specific context data. Plugins can store and retrieve their own data without key collisions.
func GetPluginData ¶
func GetPluginData(ctx context.Context) *PluginData
GetPluginData extracts the plugin data store from the context. Returns nil if not initialized. Plugins should check for nil.
func (*PluginData) Delete ¶
func (pd *PluginData) Delete(key string)
Delete removes a key from the plugin data store.
func (*PluginData) Get ¶
func (pd *PluginData) Get(key string) any
Get retrieves a value from the plugin data store. Returns nil if the key doesn't exist.
func (*PluginData) GetBool ¶
func (pd *PluginData) GetBool(key string) bool
GetBool retrieves a bool value, returning false if not found or wrong type.
func (*PluginData) GetString ¶
func (pd *PluginData) GetString(key string) string
GetString retrieves a string value, returning empty string if not found or wrong type.
func (*PluginData) Has ¶
func (pd *PluginData) Has(key string) bool
Has checks if a key exists in the plugin data store.
func (*PluginData) Keys ¶
func (pd *PluginData) Keys() []string
Keys returns all keys in the plugin data store.
func (*PluginData) Set ¶
func (pd *PluginData) Set(key string, value any)
Set stores a value for a plugin. The key should be namespaced by plugin name. Example: pluginData.Set("jwt:token_type", "access")
type RateLimitConfig ¶
type RateLimitConfig struct {
// RequestsPerWindow is the maximum number of requests allowed per time window.
// After exceeding this, requests will receive HTTP 429 Too Many Requests.
RequestsPerWindow int
// WindowDuration is the time window for counting requests.
// Example: 100 requests per 1 minute means users can make 100 requests,
// then must wait up to 1 minute before the counter resets.
WindowDuration time.Duration
// KeyPrefix is the Redis key prefix for rate limit counters.
// This prevents collisions with other Redis data.
KeyPrefix string
// ByIP enables rate limiting by client IP address.
// Useful for preventing abuse from specific sources.
ByIP bool
// ByUser enables rate limiting by authenticated user ID.
// Requires that requests are authenticated. Unauthenticated requests
// won't be rate limited by user.
ByUser bool
// ExcludePaths are URL paths exempt from rate limiting.
// Example: ["/health", "/metrics"] for monitoring endpoints.
ExcludePaths []string
}
RateLimitConfig configures rate limiting behavior. Rate limits can be applied by IP address, user ID, or both.
func AuthRateLimitConfig ¶
func AuthRateLimitConfig() *RateLimitConfig
AuthRateLimitConfig returns stricter limits for authentication endpoints. Authentication endpoints (login, signup) should have tighter limits to prevent brute force attacks and credential stuffing.
func DefaultRateLimitConfig ¶
func DefaultRateLimitConfig() *RateLimitConfig
DefaultRateLimitConfig returns sensible defaults for general API endpoints. These limits are suitable for most read-heavy applications.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter provides distributed rate limiting functionality.
It uses Redis for distributed rate limiting across multiple application instances, with an in-memory fallback for single-instance deployments. The sliding window algorithm prevents bursty traffic from overwhelming the system.
The limiter is safe for concurrent use and should be shared across HTTP handlers.
func NewRateLimiter ¶
func NewRateLimiter(config *RateLimitConfig, redisClient *redis.Client, auditLogger AuditLogger) *RateLimiter
NewRateLimiter creates a new rate limiter with the specified configuration.
Parameters:
- config: Rate limiting configuration. If nil, uses defaults.
- redisClient: Redis client for distributed rate limiting. If nil, uses in-memory fallback (only suitable for single-instance deployments).
- auditLogger: Logger for recording rate limit violations. If nil, uses a no-op logger.
The returned RateLimiter is safe for concurrent use. For multi-instance deployments, a Redis client must be provided to enforce limits across all instances.
func (*RateLimiter) Stop ¶
func (rl *RateLimiter) Stop()
Stop stops the rate limiter cleanup goroutine
type RedisConfig ¶
type RedisConfig struct {
// Host is the Redis server hostname or IP (e.g., "localhost", "redis.example.com")
Host string
// Port is the Redis server port (default: 6379)
Port int
// Password for Redis authentication (empty if no auth required)
Password string
// DB is the Redis database number (0-15, default: 0)
DB int
}
RedisConfig defines Redis connection parameters. Redis is used for session caching and distributed rate limiting.
type RedisKeyManager ¶
type RedisKeyManager struct {
// contains filtered or unexported fields
}
RedisKeyManager provides Redis-backed key-value storage with automatic expiration.
This is the recommended KeyManager implementation for production because:
- Data persists across server restarts (if Redis is configured for persistence)
- Shared across multiple server instances
- Automatic expiry with TTL support
- High performance with low latency
Use this for:
- Production deployments
- Distributed systems with multiple servers
- Any data that needs automatic expiry (OAuth states, OTP codes, etc.)
Example:
redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
keyManager := core.NewRedisKeyManager(redisClient)
func NewRedisKeyManager ¶
func NewRedisKeyManager(client *redis.Client) *RedisKeyManager
NewRedisKeyManager creates a new Redis-backed key manager.
The provided Redis client should be already configured and connected. The KeyManager will use the client for all operations.
Example:
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
keyManager := core.NewRedisKeyManager(redisClient)
func (*RedisKeyManager) Delete ¶
func (m *RedisKeyManager) Delete(ctx context.Context, key string) error
Delete removes a value from Redis.
This is idempotent - deleting a non-existent key does nothing and returns no error.
Example:
// Delete OAuth state after use to prevent replay attacks keyManager.Delete(ctx, "oauth:state:abc123")
func (*RedisKeyManager) Get ¶
Get retrieves a value from Redis.
Returns an error if:
- Key doesn't exist (redis.Nil → AuthErrorCodeInternal)
- Redis operation fails (network error, etc.)
Example:
value, err := keyManager.Get(ctx, "oauth:state:abc123")
if err != nil {
// Key doesn't exist or Redis error
}
func (*RedisKeyManager) Set ¶
func (m *RedisKeyManager) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error
Set stores a value in Redis with optional automatic expiration.
The expiry parameter controls key lifetime:
- expiry > 0: Key expires after the specified duration
- expiry = 0: Key never expires (persists indefinitely)
Redis will automatically delete expired keys using its eviction policy.
Example:
// OAuth state valid for 10 minutes keyManager.Set(ctx, "oauth:state:abc123", stateData, 10*time.Minute) // Email verification code valid for 1 hour keyManager.Set(ctx, "email:verify:[email protected]", []byte("123456"), time.Hour)
type RegisterRequest ¶
type RegisterRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
RegisterRequest represents the JSON payload for email+password registration.
type RegisterResult ¶
type RegisterResult struct {
// User is the newly created user
User auth.User
// Session is the newly created session (auto-login)
Session *auth.Session
// Token is the session token
Token string
}
RegisterResult contains the result of an email+password registration.
type RequestBodyMeta ¶
type RequestBodyMeta struct {
// Description explains what the request body contains
Description string
// Required indicates if a request body must be provided
Required bool
// Schema is either:
// - A string with the schema name (e.g., "CreateUserRequest")
// - An inline schema definition (struct or map)
Schema any
}
RequestBodyMeta describes the expected request body for an API endpoint. Used by the OpenAPI plugin for automatic documentation generation.
type RequestMeta ¶
type RequestMeta struct {
// RequestID is a unique identifier for this request (for tracing)
RequestID string
// IPAddress is the client's IP address
IPAddress string
// UserAgent is the client's User-Agent header
UserAgent string
// Method is the HTTP method (GET, POST, etc.)
Method string
// Path is the request path
Path string
}
RequestMeta contains metadata about the current request. This is useful for audit logging, rate limiting, and plugin access.
func GetRequestMeta ¶
func GetRequestMeta(ctx context.Context) *RequestMeta
GetRequestMeta extracts the request metadata from the context. Returns nil if not set.
type Response ¶
type Response struct {
// Success indicates if the request completed successfully
Success bool `json:"success"`
// Message contains a human-readable success message (optional)
Message string `json:"message,omitempty"`
// Error contains a human-readable error message (optional, Success=false)
Error string `json:"error,omitempty"`
// Data contains the response payload (optional, Success=true)
Data any `json:"data,omitempty"`
}
Response represents a standard JSON API response structure. This provides a consistent format across all Aegis endpoints.
type ResponseMeta ¶
type ResponseMeta struct {
// Description explains what this response represents
Description string
// Schema is either:
// - A string with the schema name (e.g., "User", "Error")
// - An inline schema definition (struct or map)
Schema any
}
ResponseMeta describes a possible response for an API endpoint. Used by the OpenAPI plugin for automatic documentation generation.
type RouteMetadata ¶
type RouteMetadata struct {
// Method is the HTTP method (GET, POST, PUT, DELETE, PATCH, etc.)
Method string
// Path is the route path (e.g., "/auth/logout", "/users/:id")
Path string
// Summary is a short one-line description of the endpoint
Summary string
// Description is a detailed explanation of what the endpoint does
Description string
// Tags are used for grouping operations in documentation (e.g., ["Auth", "Users"])
Tags []string
// Protected indicates if the endpoint requires authentication
Protected bool
// RequestBody describes the expected request body (optional)
RequestBody *RequestBodyMeta
// Responses maps HTTP status codes to response descriptions
// Example: {"200": {...}, "401": {...}, "500": {...}}
Responses map[string]*ResponseMeta
}
RouteMetadata contains OpenAPI documentation metadata for a route.
This metadata enables automatic API documentation generation via the OpenAPI plugin. Developers annotate routes with this metadata, and the OpenAPI spec is automatically generated.
Example:
metadata := &core.RouteMetadata{
Method: "POST",
Path: "/auth/login",
Summary: "Authenticate user",
Description: "Login with email and password",
Tags: []string{"Authentication"},
Protected: false,
RequestBody: &core.RequestBodyMeta{
Description: "Login credentials",
Required: true,
Schema: "LoginRequest",
},
Responses: map[string]*core.ResponseMeta{
"200": {Description: "Successful login", Schema: "LoginResponse"},
"401": {Description: "Invalid credentials", Schema: "Error"},
},
}
type SanitizationConfig ¶
type SanitizationConfig struct {
// MaxLength is the maximum allowed length for sanitized strings (0 = no limit)
MaxLength int
// AllowUnicode determines if non-ASCII Unicode characters are permitted
AllowUnicode bool
// StripHTML removes all HTML tags from input
StripHTML bool
// NormalizeWhitespace collapses multiple spaces into single spaces
NormalizeWhitespace bool
// TrimWhitespace removes leading and trailing whitespace
TrimWhitespace bool
}
SanitizationConfig controls the behavior of sanitization functions.
func DefaultSanitizationConfig ¶
func DefaultSanitizationConfig() *SanitizationConfig
DefaultSanitizationConfig returns a secure default configuration.
Defaults:
- MaxLength: 1000 characters (prevents DoS via large inputs)
- AllowUnicode: true (supports international users)
- StripHTML: true (prevents XSS)
- NormalizeWhitespace: true (cleans up formatting)
- TrimWhitespace: true (removes accidental spaces)
type SchemaRequirement ¶
type SchemaRequirement struct {
// Name is a human-readable identifier for this requirement
// Example: "Table 'users' exists", "Column 'accounts.password_hash' exists"
Name string
// Schema is the database schema name (optional, for multi-schema databases)
Schema string
// Table is the table name being validated (optional, for documentation)
Table string
// Query is the SQL query to execute
// Should succeed without error if the requirement is met
// Example: "SELECT 1 FROM users WHERE 1=0"
Query string
// Description explains what the validation failure means
// This is shown in the error message if the query returns no rows
// Example: "Table 'users' does not exist or is empty"
Description string
}
SchemaRequirement defines a single schema validation check.
Each requirement represents one validation rule (table exists, column exists, index exists, etc.). The validator executes the Query and expects it to succeed without error for the requirement to pass.
func SchemaRequirements ¶
func SchemaRequirements() []SchemaRequirement
SchemaRequirements returns the schema requirements for core Aegis tables.
This validates the existence of:
- Tables: user, accounts, verification, session
- All required columns in each table
Call this during application startup to ensure the database is properly migrated:
validator := core.NewSchemaValidator(db)
if err := validator.ValidateRequirements(ctx, core.SchemaRequirements()); err != nil {
log.Fatal("Schema validation failed:", err)
}
func ValidateColumnExists ¶
func ValidateColumnExists(tableName, columnName string) SchemaRequirement
ValidateColumnExists creates a requirement to check if a column exists in a table.
This queries `information_schema.columns` for the given table and column. The actual query generated is:
SELECT column_name FROM information_schema.columns WHERE table_name = '<table>' AND column_name = '<column>' LIMIT 1
Example:
requirement := core.ValidateColumnExists("users", "email")
// Will check: SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'email' LIMIT 1
func ValidateTableExists ¶
func ValidateTableExists(tableName string) SchemaRequirement
ValidateTableExists creates a requirement to check if a table exists.
This queries `information_schema.tables` for the given table name. The actual query generated is:
SELECT table_name FROM information_schema.tables WHERE table_name = '<name>' LIMIT 1
Example:
requirement := core.ValidateTableExists("users")
// Will check: SELECT table_name FROM information_schema.tables WHERE table_name = 'users' LIMIT 1
type SchemaValidator ¶
type SchemaValidator struct {
// contains filtered or unexported fields
}
SchemaValidator provides database schema validation for Aegis.
This validator helps ensure that the required database tables and columns exist before the application starts. It's used by:
- Core Aegis (validates user, accounts, session, verification tables)
- Plugins (each plugin can validate its own schema requirements)
The validator executes SQL queries to check for table/column existence, collecting all validation errors before failing. This provides complete error visibility instead of failing on the first missing table.
Example:
validator := core.NewSchemaValidator(db)
err := validator.ValidateRequirements(ctx, core.SchemaRequirements())
if err != nil {
log.Fatal("Database schema validation failed:", err)
}
func NewSchemaValidator ¶
func NewSchemaValidator(db *sql.DB) *SchemaValidator
NewSchemaValidator creates a new schema validator for the given database.
The validator will execute queries against this database connection to validate schema requirements.
func (*SchemaValidator) ValidateRequirements ¶
func (v *SchemaValidator) ValidateRequirements(ctx context.Context, requirements []SchemaRequirement) error
ValidateRequirements validates a list of schema requirements.
This method runs all validations and collects ALL errors before returning. This provides complete visibility into schema problems instead of failing on the first error.
Returns nil if all requirements pass, or an error listing all failures.
Example:
requirements := []core.SchemaRequirement{
core.ValidateTableExists("users"),
core.ValidateColumnExists("users", "email"),
core.ValidateColumnExists("users", "password_hash"),
}
err := validator.ValidateRequirements(ctx, requirements)
type SessionConfig ¶
type SessionConfig struct {
// SessionExpiry is how long session tokens remain valid.
// After this, users must re-login or use a refresh token.
// Typical: 24 hours for session, 7 days for refresh
SessionExpiry time.Duration
// RefreshExpiry is how long refresh tokens remain valid.
// Enables "remember me" functionality by allowing new sessions
// to be created without re-entering credentials.
RefreshExpiry time.Duration
// CookieSettings configures HTTP session cookies
CookieSettings CookieSettings
// Redis connection for session caching (optional).
// If nil, sessions are always loaded from database (slower but simpler).
Redis *RedisConfig
}
SessionConfig defines session and cookie management settings.
func DefaultSessionConfig ¶
func DefaultSessionConfig() *SessionConfig
DefaultSessionConfig returns default session configuration
type SessionModel ¶
type SessionModel interface {
// GetID returns the unique identifier for this session
GetID() string
// SetID assigns a unique identifier to this session
SetID(string)
// GetUserID returns the ID of the user this session belongs to
GetUserID() string
// SetUserID assigns the owning user's ID
SetUserID(string)
// GetToken returns the session authentication token
GetToken() string
// SetToken assigns the session authentication token
SetToken(string)
// GetRefreshToken returns the refresh token for session renewal
GetRefreshToken() string
// SetRefreshToken assigns the refresh token
SetRefreshToken(string)
// SetCreatedAt assigns the creation timestamp
SetCreatedAt(time.Time)
// GetExpiresAt returns when this session expires
GetExpiresAt() time.Time
// SetExpiresAt assigns the session expiration time
SetExpiresAt(time.Time)
// SetIPAddress assigns the client IP address for security tracking
SetIPAddress(string)
// SetUserAgent assigns the client user agent for security tracking
SetUserAgent(string)
}
SessionModel defines the required methods for a session model implementation. Sessions track authenticated user activity and enable stateful authentication.
type SessionService ¶
type SessionService struct {
// contains filtered or unexported fields
}
SessionService manages user session lifecycle including creation, validation, refresh, and invalidation. It provides optional Redis-based caching for high-performance session lookups in high-traffic applications.
Key features:
- Token-based authentication with access and refresh tokens
- Optional Redis caching layer for fast session validation
- Session expiry and refresh token rotation
- IP address and user agent tracking for security auditing
- Bulk session invalidation (logout all devices)
The service is safe for concurrent use and should be shared across handlers.
func NewSessionService ¶
func NewSessionService(userStore auth.UserStore, sessionStore auth.SessionStore, cfg *SessionConfig, auditLogger AuditLogger) *SessionService
NewSessionService creates a new session service with optional Redis caching.
Parameters:
- userStore: Storage for user lookups during session validation
- sessionStore: Storage for session persistence
- cfg: Session configuration (expiry, Redis settings). Uses defaults if nil.
- auditLogger: Logger for security events. Uses no-op if nil.
If cfg.Redis is provided, a Redis client is created for session caching. This significantly improves performance by avoiding database queries for every authenticated request.
func (*SessionService) CreateSession ¶
CreateSession creates a new authenticated session for a user.
Generates cryptographically secure random tokens for both session access and refresh tokens. The session is persisted to the database and optionally cached in Redis for fast subsequent lookups.
Parameters:
- ctx: Request context for cancellation. Must contain RequestMeta (populated by AegisContextMiddleware) for IP address and user agent.
- user: The authenticated user to create a session for
Returns the created session with populated Token and RefreshToken fields. These tokens should be sent to the client (typically via HTTP-only cookies or Authorization header).
Logs a successful login audit event upon session creation.
func (*SessionService) DeleteSession ¶
func (s *SessionService) DeleteSession(ctx context.Context, token string) error
DeleteSession deletes a session and invalidates cache
func (*SessionService) DeleteUserSessions ¶
func (s *SessionService) DeleteUserSessions(ctx context.Context, userID string) error
DeleteUserSessions deletes all sessions for a user
func (*SessionService) EnableBearerAuth ¶
func (s *SessionService) EnableBearerAuth()
EnableBearerAuth enables Bearer token authentication for sessions.
func (*SessionService) GetConfig ¶
func (s *SessionService) GetConfig() *SessionConfig
GetConfig returns the session configuration.
func (*SessionService) GetCookieManager ¶
func (s *SessionService) GetCookieManager() *CookieManager
GetCookieManager returns the cookie manager.
func (*SessionService) GetRedisClient ¶
func (s *SessionService) GetRedisClient() *redis.Client
GetRedisClient returns the Redis client used for session storage.
func (*SessionService) GetUserSessions ¶
func (s *SessionService) GetUserSessions(ctx context.Context, userID string) ([]*auth.Session, error)
GetUserSessions retrieves all active sessions for a user
func (*SessionService) IsBearerAuthEnabled ¶
func (s *SessionService) IsBearerAuthEnabled() bool
IsBearerAuthEnabled checks if Bearer token authentication is enabled.
func (*SessionService) Logout ¶
func (s *SessionService) Logout(ctx context.Context, token string) error
Logout deletes a session by token (alias for DeleteSession)
func (*SessionService) RefreshSession ¶
func (s *SessionService) RefreshSession(ctx context.Context, refreshToken string) (*auth.Session, error)
RefreshSession refreshes a session using a refresh token
func (*SessionService) RevokeSessionByID ¶
func (s *SessionService) RevokeSessionByID(ctx context.Context, userID, sessionID string) error
RevokeSessionByID revokes a specific session for a user by its ID.
This method verifies that the session belongs to the user before revocation.
Parameters:
- ctx: Request context
- userID: The user ID who owns the session
- sessionID: The session ID to revoke
Returns:
- error: AuthErrorCodeSessionNotFound if session does not exist or belong to user, or database error.
func (*SessionService) ValidateSession ¶
func (s *SessionService) ValidateSession(ctx context.Context, tokenString string) (*auth.Session, *auth.User, error)
ValidateSession validates a session token and returns the session and user.
The validation flow:
- Check Redis cache if available (fast path)
- Fall back to database lookup if not cached
- Verify session hasn't expired
- Load associated user data
- Cache the session in Redis for future requests
This method is called on every authenticated request, so caching is critical for performance in production deployments.
Parameters:
- ctx: Request context for cancellation
- tokenString: The session token to validate
Returns:
- *auth.Session: The valid session
- *auth.User: The user associated with this session
- error: AuthErrorCodeTokenExpired if expired, AuthErrorCodeSessionInvalid if not found, AuthErrorCodeUserNotFound if user was deleted
type SessionWithUser ¶
type SessionWithUser struct {
Session *auth.Session `json:"session"`
User *EnrichedUser `json:"user"`
}
SessionWithUser combines session and enriched user data for API responses. This is returned by session validation endpoints. The user data includes all extension fields flattened.
func (*SessionWithUser) ToAPIResponse ¶
func (swu *SessionWithUser) ToAPIResponse() map[string]any
ToAPIResponse returns a map suitable for JSON API responses. Session includes all session fields, user includes all extension fields flattened.
func (*SessionWithUser) ToAPIResponseFiltered ¶
func (swu *SessionWithUser) ToAPIResponseFiltered(config *UserFieldsConfig) map[string]any
ToAPIResponseFiltered returns a map with optional user field filtering. Session data is always fully included; only user extension fields are filtered.
type StaticKeyManager ¶
type StaticKeyManager struct {
// contains filtered or unexported fields
}
StaticKeyManager provides in-memory key-value storage.
WARNING: This is NOT suitable for production use because:
- Data is lost on server restart
- Not shared across multiple server instances
- No persistence or durability
Use this for:
- Local development and testing
- Single-server deployments with non-critical data
- Unit tests that need fast in-memory storage
For production, use RedisKeyManager instead.
Note: Unlike Redis, expiry is NOT supported - all entries persist until manually deleted or the server restarts.
func NewStaticKeyManager ¶
func NewStaticKeyManager() (*StaticKeyManager, error)
NewStaticKeyManager creates a new in-memory key manager.
This manager stores all data in a Go map with no persistence or expiry. Data is lost when the server stops.
Example:
keyManager, _ := core.NewStaticKeyManager()
keyManager.Set(ctx, "key", []byte("value"), 0)
func (*StaticKeyManager) Delete ¶
func (m *StaticKeyManager) Delete(_ context.Context, key string) error
Delete removes a value from in-memory storage. Does nothing if the key doesn't exist.
func (*StaticKeyManager) Get ¶
Get retrieves a value from in-memory storage.
Returns an error if the key doesn't exist.
type UserFieldsConfig ¶
type UserFieldsConfig struct {
// Fields is the list of extension field names to include in user responses.
// Plugins will only add fields that are in this list (if configured).
// If nil or empty, all plugin fields are included (default behavior).
Fields []string
}
UserFieldsConfig defines which extension fields plugins should add to EnrichedUser. This allows users to configure what additional data appears in user API responses.
Example configuration:
UserFields: &core.UserFieldsConfig{
Fields: []string{"role", "permissions", "organizations", "verified"},
}
This produces JSON responses like:
{
"id": "user_123",
"email": "[email protected]",
"role": "admin",
"permissions": ["read", "write"],
"organizations": ["org1", "org2"],
"verified": true
}
func DefaultUserFieldsConfig ¶
func DefaultUserFieldsConfig() *UserFieldsConfig
DefaultUserFieldsConfig returns default user fields configuration. By default, all extension fields are included in responses.
type UserModel ¶
type UserModel interface {
// GetID returns the unique identifier for this user
GetID() string
// SetID assigns a unique identifier to this user
SetID(string)
// GetEmail returns the user's email address
GetEmail() string
// SetEmail assigns an email address to this user
SetEmail(string)
// GetName returns the user's display name
GetName() string
// SetName assigns a display name to this user
SetName(string)
// SetCreatedAt assigns the creation timestamp
SetCreatedAt(time.Time)
// SetUpdatedAt assigns the last modification timestamp
SetUpdatedAt(time.Time)
}
UserModel defines the required methods for a user model implementation. Any type implementing this interface can be used as a user in the authentication system.
type UserService ¶
type UserService struct {
// contains filtered or unexported fields
}
UserService provides high-level user management operations. It orchestrates user creation, deletion, and updates while coordinating with AccountStore and SessionStore to maintain data consistency.
Key responsibilities:
- User CRUD operations
- Password account creation during user signup
- Cascading deletion of accounts and sessions
- Audit logging of user lifecycle events
The service is safe for concurrent use.
func NewUserService ¶
func NewUserService(userStore auth.UserStore, accountStore auth.AccountStore, sessionStore auth.SessionStore, hashConfig *PasswordHasherConfig, authConfig *AuthConfig, auditLogger AuditLogger) *UserService
NewUserService creates a new user service with the specified dependencies.
func (*UserService) CreateUser ¶
func (s *UserService) CreateUser(ctx context.Context, user auth.User, password string) (auth.User, error)
CreateUser creates a new user with a password-based authentication account.
This is the primary method for email/password signup flows. It:
- Assigns a unique ID if not provided
- Sets creation and update timestamps
- Persists the user to storage
- Hashes the password using Argon2id
- Creates a password-based account linked to the user
The password is hashed with the configured Argon2id parameters before storage. The account is created with provider="credentials" to distinguish it from OAuth accounts.
Parameters:
- ctx: Request context for cancellation
- user: User model with email, name, etc. (ID optional)
- password: Plaintext password to hash and store
Returns the created user. If creation fails, the user account is not created (no partial state).
func (*UserService) CreateUserWithEmail ¶
func (s *UserService) CreateUserWithEmail(ctx context.Context, name, email, password string) (auth.User, error)
CreateUserWithEmail is a convenience method for creating a user with email/password.
This is a simplified wrapper around CreateUser for the common case of email/password signup. It constructs the user model from individual fields and delegates to CreateUser.
Parameters:
- ctx: Request context
- name: User's display name
- email: User's email address (should be unique)
- password: Plaintext password
Example:
user, err := userService.CreateUserWithEmail(ctx, "John Doe", "[email protected]", "secret123")
func (*UserService) CreateUserWithoutPassword ¶
func (s *UserService) CreateUserWithoutPassword(ctx context.Context, user auth.User) (auth.User, error)
CreateUserWithoutPassword creates a new user without any authentication account.
This is used for OAuth-only users or when accounts will be created separately. Common scenarios:
- OAuth signup (Google, GitHub, etc.) where password is not needed
- Admin-created users where credentials are set later
- Service accounts or system users
Note: The user won't be able to log in with email/password until a password account is created separately.
Parameters:
- ctx: Request context
- user: User model (ID will be generated if not provided)
func (*UserService) DeleteUser ¶
func (s *UserService) DeleteUser(ctx context.Context, id string) error
DeleteUser deletes a user and all associated data (accounts and sessions).
This performs a cascading delete to maintain referential integrity:
- Delete all sessions (logs user out from all devices)
- Delete all accounts (credentials, OAuth connections, etc.)
- Delete the user record itself
The deletion order ensures that foreign key constraints are satisfied. If any step fails, subsequent steps are still attempted (best-effort cleanup).
Parameters:
- ctx: Request context
- id: User ID to delete
Returns an error only if the user deletion itself fails. Session and account deletion errors are logged but don't fail the operation.
func (*UserService) GetUserByEmail ¶
GetUserByEmail retrieves a user by their email address.
func (*UserService) GetUserByID ¶
GetUserByID retrieves a user by their unique ID.
func (*UserService) UpdateUser ¶
UpdateUser updates an existing user's information.
func (*UserService) UpdateUserEmail ¶
func (s *UserService) UpdateUserEmail(ctx context.Context, userID, email string) error
UpdateUserEmail updates a user's email
type ValidationError ¶
type ValidationError struct {
// Field is the name of the field that failed validation
Field string
// Message is a human-readable description of what's wrong
Message string
}
ValidationError represents a validation error for a specific field. Used when input validation fails for user-provided data.
func (ValidationError) Error ¶
func (e ValidationError) Error() string
type ValidationErrors ¶
type ValidationErrors []ValidationError
ValidationErrors represents multiple validation errors. Useful when validating entire request bodies and returning all errors at once rather than failing on the first issue.
func GetValidationErrors ¶
func GetValidationErrors(err error) ValidationErrors
GetValidationErrors extracts all validation errors from an error
func (ValidationErrors) Error ¶
func (e ValidationErrors) Error() string
func (ValidationErrors) Errors ¶
func (e ValidationErrors) Errors() []ValidationError
Errors returns all validation errors as a slice for iteration.
type VerificationModel ¶
type VerificationModel interface {
// GetID returns the unique identifier for this verification
GetID() string
// SetID assigns a unique identifier to this verification
SetID(string)
// GetToken returns the verification token/code
GetToken() string
// SetToken assigns the verification token/code
SetToken(string)
// GetIdentifier returns the target of verification (e.g., email address)
GetIdentifier() string
// SetIdentifier assigns the target identifier
SetIdentifier(string)
// SetCreatedAt assigns the creation timestamp
SetCreatedAt(time.Time)
// GetExpiresAt returns when this verification expires
GetExpiresAt() time.Time
// SetExpiresAt assigns the verification expiration time
SetExpiresAt(time.Time)
}
VerificationModel defines the required methods for a verification model implementation. Verifications are temporary tokens used for email confirmation, password resets, etc.
type VerificationService ¶
type VerificationService struct {
// contains filtered or unexported fields
}
VerificationService manages temporary verification tokens for various flows:
- Email verification after signup
- Password reset tokens
- OTP (one-time password) codes
- Magic link tokens
- Custom verification workflows
All verifications have:
- A unique token/code
- An identifier (email, phone, etc.)
- A type ("email", "reset", "otp", etc.)
- An expiration time
Verifications are single-use and should be deleted or invalidated after use.
func NewVerificationService ¶
func NewVerificationService(store auth.VerificationStore, auditLogger AuditLogger) *VerificationService
NewVerificationService creates a new verification service.
func (*VerificationService) CreateVerification ¶
func (s *VerificationService) CreateVerification(ctx context.Context, identifier, vType string, expiry time.Duration, customToken *string) (auth.Verification, error)
CreateVerification creates a new verification token for a specific purpose.
Generates a cryptographically secure random token (or uses a custom token if provided). The verification is stored with an expiration time for automatic cleanup.
Common use cases:
- Email verification: CreateVerification(ctx, email, "email", 24*time.Hour, nil)
- Password reset: CreateVerification(ctx, email, "reset", 1*time.Hour, nil)
- Custom OTP: CreateVerification(ctx, phone, "otp", 10*time.Minute, &customCode)
Parameters:
- ctx: Request context
- identifier: Target being verified (email, phone, user ID, etc.)
- vType: Verification type ("email", "reset", "otp", etc.)
- expiry: How long the token is valid
- customToken: Optional custom token (if nil, random hex token is generated)
Returns the created verification with populated token.
func (*VerificationService) DeleteVerification ¶
func (s *VerificationService) DeleteVerification(ctx context.Context, id string) error
DeleteVerification permanently deletes a verification token.
Use this for cleanup after successful verification or when canceling a verification flow.
Parameters:
- ctx: Request context
- id: Verification ID to delete
func (*VerificationService) InvalidateVerification ¶
func (s *VerificationService) InvalidateVerification(ctx context.Context, identifier, vType string) error
InvalidateVerification marks all tokens of a specific type for an identifier as invalid.
This prevents token reuse after successful verification. For example, after a user verifies their email, all pending email verification tokens for that address should be invalidated.
Parameters:
- ctx: Request context
- identifier: The target identifier (email, phone, etc.)
- vType: The verification type to invalidate ("email", "reset", etc.)
func (*VerificationService) ValidateVerification ¶
func (s *VerificationService) ValidateVerification(ctx context.Context, token string) (auth.Verification, error)
ValidateVerification validates a token and returns the verification if valid.
Checks:
- Token exists in storage
- Token has not expired
After successful validation, the caller should typically:
- Perform the verified action (activate account, reset password, etc.)
- Invalidate the token to prevent reuse
Parameters:
- ctx: Request context
- token: The token string to validate
Returns:
- The verification record if valid
- AuthErrorCodeTokenExpired if expired
- Error if token not found