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 BoolPtr(b bool) *bool
- func CSRFMiddleware(cfg CSRFConfig) func(http.Handler) http.Handler
- 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 GetClientIPTrusted(r *http.Request, trustedProxies []string) 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 HashTokenHex(token string) string
- func IsAuthError(err error) bool
- func IsContextInitialized(ctx context.Context) bool
- func IsEncrypted(value string) bool
- func IsHashedToken(s string) 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 OpenWithKey(key []byte, value string) (string, error)
- 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 SanitizeSQLIdentifier(name string) string
- func SanitizeString(input string, config *SanitizationConfig) string
- func SanitizeURL(url string) string
- func SanitizeUsername(username string, maxLength int) string
- func SealWithKey(key []byte, plaintext string) (string, error)
- func SecurityHeadersMiddleware(cfg *SecurityHeadersConfig) func(http.Handler) http.Handler
- 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 BearerTokenValidator
- type CSRFConfig
- type ColumnSpec
- 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 Logger
- 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 PaginatedResponse
- 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 RequestMeta
- type Response
- type SanitizationConfig
- type SchemaRequirement
- func SchemaRequirements() []SchemaRequirement
- func ValidateColumnExists(tableName, columnName string) SchemaRequirement
- func ValidateColumnExistsForDialect(dialect, tableName, columnName string) SchemaRequirement
- func ValidateColumnSpec(tableName, columnName string, spec ColumnSpec) SchemaRequirement
- func ValidateColumnSpecForDialect(dialect, tableName, columnName string, spec ColumnSpec) SchemaRequirement
- func ValidateTableExists(tableName string) SchemaRequirement
- func ValidateTableExistsForDialect(dialect, tableName string) SchemaRequirement
- type SchemaValidator
- type SecurityHeadersConfig
- type SessionConfig
- type SessionModel
- type SessionRefreshResponse
- type SessionService
- func (s *SessionService) AddBearerTokenValidator(v BearerTokenValidator)
- func (s *SessionService) CountUserSessions(ctx context.Context, userID string) (int, error)
- 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) GetBearerTokenValidators() []BearerTokenValidator
- 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, offset, limit int) ([]*auth.Session, error)
- func (s *SessionService) IsBearerAuthEnabled() bool
- func (s *SessionService) Logout(ctx context.Context, token string) error
- func (s *SessionService) MigrateHashSessionTokensForUser(ctx context.Context, userID string) (migrated int, err 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 ( // Common response schemas SchemaError = "Error" SchemaSuccess = "Success" )
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:
- 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
const ( SchemaDialectPostgres = "postgres" SchemaDialectMySQL = "mysql" SchemaDialectSQLite = "sqlite" )
Schema dialect constants used by the dialect-aware ValidateXxxForDialect helpers. These mirror config.Dialect values without importing the config package (which would create an import cycle, since config imports core).
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 EncryptionPrefix = "enc:v1:"
EncryptionPrefix is prepended to ciphertext produced by SealWithDerivedKey so callers can distinguish encrypted values from legacy plaintext rows during incremental migrations and switch the format later without breaking existing data.
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 BoolPtr ¶ added in v1.6.0
BoolPtr is a tiny convenience for building ColumnSpec.Nullable inline.
func CSRFMiddleware ¶ added in v1.6.0
func CSRFMiddleware(cfg CSRFConfig) func(http.Handler) http.Handler
CSRFMiddleware returns middleware that enforces CSRF protection using the signed double-submit cookie pattern. See the package-level comment for the algorithm and threat model.
Behavior:
- GET/HEAD/OPTIONS/TRACE: pass through; ensure a valid token cookie is set (issuing one if missing or invalid).
- Authorization: Bearer …: pass through unchanged. Bearer auth is not vulnerable to CSRF because browsers do not auto-attach custom Authorization headers cross-site.
- Other methods: require a valid cookie AND a matching header value. Mismatches return 403.
Panics if cfg.MasterSecret is shorter than 32 bytes.
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.
SECURITY: This function does NOT trust X-Forwarded-For or X-Real-IP headers. It returns r.RemoteAddr unchanged. Honoring proxy headers without an allowlist lets any client spoof their IP, which would break rate-limit / audit / lockout protection.
If you run Aegis behind a reverse proxy, use GetClientIPTrusted with a parsed CIDR allowlist of your trusted proxy addresses, or rely on the rate-limiter's built-in TrustedProxies handling.
func GetClientIPTrusted ¶ added in v1.6.0
GetClientIPTrusted returns the client IP, honoring X-Forwarded-For / X-Real-IP only when the immediate peer (r.RemoteAddr) is contained in one of the trustedProxies CIDR ranges. trustedProxies entries may be in CIDR form ("10.0.0.0/8") or bare IP form ("10.0.0.5"); invalid entries are silently ignored.
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 HashTokenHex ¶ added in v1.6.0
HashTokenHex returns the SHA-256 hex digest of a token, suitable for at-rest persistence and cache keying. The output is 64 lowercase hex characters; an empty input yields an empty string. SHA-256 is appropriate here because the input is high-entropy random bytes (not a low-entropy password) — no per-token salt or cost factor is required for collision resistance or pre-image protection at this entropy level.
func IsContextInitialized ¶
IsContextInitialized checks if AegisContextMiddleware has been run. This is used internally to ensure proper middleware chain ordering.
func IsEncrypted ¶ added in v1.6.0
IsEncrypted reports whether value was produced by SealWithDerivedKey.
func IsHashedToken ¶ added in v1.6.0
IsHashedToken reports whether s looks like a SHA-256 hex digest produced by HashTokenHex. Used by migration helpers to skip already-hashed rows for idempotency.
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 OpenWithKey ¶ added in v1.6.0
OpenWithKey reverses SealWithKey using the supplied 32-byte key.
Values that do not carry the EncryptionPrefix are treated as legacy plaintext and returned unchanged so callers can transparently read rows written before encryption was enabled. Re-writing such a row with SealWithKey will upgrade it.
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: "alice@example.com" -> "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(" John.Doe@EXAMPLE.com ")
// Output: "john.doe@example.com"
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 SanitizeSQLIdentifier ¶ added in v1.6.0
SanitizeSQLIdentifier validates that name is a safe SQL identifier (table or column name) and returns it unchanged. It panics if name contains any character outside of [A-Za-z0-9_] or does not start with a letter / underscore.
This helper exists because the schema-validation requirement helpers (ValidateTableExists, ValidateColumnExists, …) build their probe queries via fmt.Sprintf — there is no portable way to parameterise an identifier across information_schema, sqlite_master, and pragma_*. Identifiers come from compile-time plugin code, never from user input, so a panic on an invalid identifier is the correct fail-fast behavior: it surfaces the bug at startup instead of silently producing a malformed (or worse, injectable) query.
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 SealWithKey ¶ added in v1.6.0
SealWithKey encrypts plaintext using AES-256-GCM with the supplied 32-byte key (typically obtained via DeriveSecret with a purpose-specific label).
The returned string has the form "enc:v1:" + base64(nonce|ciphertext|tag). An empty plaintext is returned as the empty string unchanged so callers can persist optional fields without forcing them through the codec.
func SecurityHeadersMiddleware ¶ added in v1.6.0
func SecurityHeadersMiddleware(cfg *SecurityHeadersConfig) func(http.Handler) http.Handler
SecurityHeadersMiddleware returns middleware that emits common HTTP security headers on every response.
Pass nil to use DefaultSecurityHeadersConfig().
The middleware writes headers BEFORE calling next.ServeHTTP, so the downstream handler may overwrite any header it manages explicitly (e.g. a route that needs a custom CSP).
Example:
router.Use(core.SecurityHeadersMiddleware(nil)) cfg := core.DefaultSecurityHeadersConfig() cfg.ContentSecurityPolicy = "default-src 'self'" router.Use(core.SecurityHeadersMiddleware(&cfg))
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. The response envelope matches the core.Response structure and the OpenAPI Error schema.
Example:
core.WriteJSONError(w, 400, "Invalid request")
// Output: {"success": false, "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, transactor auth.Transactor, logger Logger) *AccountService
NewAccountService creates a new account service with the specified dependencies.
transactor is optional: when non-nil it enables fully atomic password updates (account write + session purge in one DB transaction). When nil, UpdatePassword still runs both operations but in sequence and surfaces session-purge errors instead of swallowing them, so the caller can decide whether to retry. logger may be nil; a no-op logger is substituted in that case.
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.
Atomicity:
- When the underlying store supports transactions (the default SQL backend does), the password write and the session purge run in a single DB transaction. Either both happen or neither does, so an attacker holding a valid session can never end up in a state where the new password is in effect but the old session is still alive.
- When transactions are not available (custom store implementations that do not implement auth.Transactor), the operations run in sequence; if the session purge fails the password update is rolled back manually by re-saving the previous hash and the original session-purge error is returned. This avoids leaving the account in the dangerous half-rotated state where the password changed but old sessions remain valid.
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, logger Logger) *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 BearerTokenValidator ¶ added in v1.5.1
type BearerTokenValidator interface {
ValidateBearerToken(ctx context.Context, token string) (*auth.User, *auth.Session, error)
}
BearerTokenValidator is an optional interface for validating non-session bearer tokens such as JWT access tokens. When registered with SessionService via SetBearerTokenValidator, it is called by AuthMiddleware before falling back to the opaque session-token database lookup.
Implementations (e.g., the JWT plugin) should:
- Cryptographically verify the token
- Return the authenticated user and, if applicable, a synthetic session
- Return a non-nil error if the token is invalid or expired
A nil *auth.Session return value is valid for fully-stateless token schemes.
type CSRFConfig ¶ added in v1.6.0
type CSRFConfig struct {
// MasterSecret is the application's master secret. The middleware
// derives a CSRF-specific HMAC key from it via DeriveSecret. Must
// be at least 32 bytes; shorter secrets cause CSRFMiddleware to
// panic at construction time, which fails fast at startup.
MasterSecret []byte
// CookieName is the cookie that carries the signed token.
// Default: "aegis_csrf".
CookieName string
// HeaderName is the request header the middleware reads on unsafe
// methods. Default: "X-CSRF-Token".
HeaderName string
// CookiePath restricts the CSRF cookie to a path. Default: "/".
CookiePath string
// CookieDomain optionally scopes the cookie to a domain.
CookieDomain string
// SameSite controls the cookie's SameSite attribute. Default:
// http.SameSiteLaxMode, which is the recommended balance for
// browser apps that perform top-level navigation.
SameSite http.SameSite
// Secure marks the cookie Secure (HTTPS-only). Default: true.
// Set to false ONLY for local development.
Secure bool
// MaxAge is the cookie lifetime. Default: 12h.
MaxAge time.Duration
// TrustedOrigins, when non-empty, additionally requires the request
// Origin (or Referer, fallback) to match one of these absolute
// origins (e.g. "https://app.example.com"). This is defense in
// depth on top of the double-submit check.
TrustedOrigins []string
// SkipPaths is an optional list of exact paths to bypass entirely.
// Use sparingly; the standard exemptions (safe methods + Bearer)
// already cover most legitimate cases.
SkipPaths []string
}
CSRFConfig configures the CSRF protection middleware.
func DefaultCSRFConfig ¶ added in v1.6.0
func DefaultCSRFConfig() CSRFConfig
DefaultCSRFConfig returns a CSRFConfig populated with safe defaults. MasterSecret must still be set by the caller.
type ColumnSpec ¶ added in v1.6.0
ColumnSpec describes the expected shape of a column for validation.
Either field may be left at its zero value to skip that part of the check:
- DataType == "": don't compare types (presence-only check).
- Nullable == nil: don't compare nullability.
DataType is matched against information_schema.columns.data_type using a case-insensitive equality test. Database-specific type aliases (e.g. "int" vs "integer", "varchar" vs "character varying") are NOT normalised — pass the canonical name your dialect reports. SQLite is not supported by information_schema and will fall back to a presence-only check.
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": "user@example.com",
"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 Logger ¶ added in v1.6.0
type Logger interface {
Info(msg string, keysAndValues ...any)
Error(msg string, keysAndValues ...any)
Debug(msg string, keysAndValues ...any)
}
Logger is the minimal logging interface used by core services for non-fatal operational events (cache errors, recoverable store errors, etc). It is structurally compatible with config.Logger so that the application-level logger can be plumbed in without an import cycle.
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. Safe to call multiple times.
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 PaginatedResponse ¶ added in v1.5.0
type PaginatedResponse[T any] struct { Items []T `json:"items"` TotalCount int `json:"totalCount"` Page int `json:"page"` Offset int `json:"offset"` Limit int `json:"limit"` }
PaginatedResponse is a standard pagination envelope for list endpoints.
It is intended to be wrapped as core.Response.Data (i.e. core.Response{Data: PaginatedResponse[T]}). This keeps pagination metadata consistent across endpoints without per-resource DTOs.
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
// FailClosed controls behavior when the backing store (e.g. Redis)
// returns an error from Allow().
//
// - false (default): fail-open. The request is allowed and the error
// is logged at WARN level. Preserves availability if Redis is flaky
// but a determined attacker can defeat brute-force protection by
// deliberately straining the backing store.
// - true: fail-closed. The request is rejected with HTTP 429. Stronger
// security guarantee at the cost of availability when the backing
// store is unhealthy. Recommended for high-value auth endpoints.
FailClosed bool
// TrustedProxies is a list of CIDR ranges (or single IPs in CIDR form,
// e.g. "10.0.0.5/32") whose X-Forwarded-For / X-Real-IP headers are
// honored. When the request's RemoteAddr is NOT within one of these
// ranges, proxy headers are ignored and the raw RemoteAddr is used as
// the client IP. Default empty = no proxy is trusted (most secure).
//
// Set this to your real proxy/load-balancer addresses in production
// when running behind a reverse proxy that sets these headers.
TrustedProxies []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, logger Logger) *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.
- logger: Optional diagnostic logger. May be nil.
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. Safe to call multiple times.
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:user@example.com", []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 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 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
func ValidateColumnExists ¶
func ValidateColumnExists(tableName, columnName string) SchemaRequirement
ValidateColumnExists creates a requirement to check if a column exists in a table.
This is the simplest column check: presence only. If you also need to detect schema drift (wrong type, unexpected NULL/NOT NULL, etc.), use ValidateColumnSpec instead — silent type changes have caused real production bugs in this codebase before.
The generated query targets information_schema, which is supported by PostgreSQL and MySQL but not by SQLite. SQLite callers should use ValidateColumnExistsForDialect with DialectSQLite (see schema_dialects.go).
func ValidateColumnExistsForDialect ¶ added in v1.6.0
func ValidateColumnExistsForDialect(dialect, tableName, columnName string) SchemaRequirement
ValidateColumnExistsForDialect builds a column-existence requirement using the appropriate metadata source for the given dialect.
SQLite returns one row per column from pragma_table_info(<table>); we filter by column name. Other dialects use information_schema.
func ValidateColumnSpec ¶ added in v1.6.0
func ValidateColumnSpec(tableName, columnName string, spec ColumnSpec) SchemaRequirement
ValidateColumnSpec creates a requirement that checks not just the existence of a column, but also (optionally) its data type and nullability. It is the recommended replacement for ValidateColumnExists when you care about detecting schema drift.
The query selects the row from information_schema.columns and uses boolean predicates to enforce the spec. A failed match returns zero rows, which the validator interprets as a failure with a descriptive message.
func ValidateColumnSpecForDialect ¶ added in v1.6.0
func ValidateColumnSpecForDialect(dialect, tableName, columnName string, spec ColumnSpec) SchemaRequirement
ValidateColumnSpecForDialect builds a column-spec requirement using the appropriate metadata source for the given dialect.
SQLite's pragma_table_info exposes `type` (text) and `notnull` (0/1) instead of the information_schema column names. We translate the spec fields onto those columns. Type comparisons remain case-insensitive-equal; SQLite stores types verbatim from the CREATE TABLE declaration, so callers should pass the exact declared type (e.g. "TEXT", "INTEGER", "BLOB").
func ValidateTableExists ¶
func ValidateTableExists(tableName string) SchemaRequirement
ValidateTableExists creates a requirement to check if a table exists.
tableName must be a valid SQL identifier ([A-Za-z_][A-Za-z0-9_]*). This panics on malformed identifiers to fail fast at startup — see SanitizeSQLIdentifier for rationale.
func ValidateTableExistsForDialect ¶ added in v1.6.0
func ValidateTableExistsForDialect(dialect, tableName string) SchemaRequirement
ValidateTableExistsForDialect builds a table-existence requirement using the appropriate metadata source for the given dialect.
PostgreSQL and MySQL use information_schema. SQLite has no information_schema; instead it exposes sqlite_master, which we query directly. Unknown dialects fall back to information_schema for backwards compatibility with the original ValidateTableExists helper.
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 SecurityHeadersConfig ¶ added in v1.6.0
type SecurityHeadersConfig struct {
// XContentTypeOptions sets the X-Content-Type-Options header.
// Recommended: "nosniff" — disables MIME sniffing.
XContentTypeOptions string
// XFrameOptions sets the X-Frame-Options header.
// Recommended: "DENY" — blocks the response from being framed
// (clickjacking protection). Use "SAMEORIGIN" if your own pages
// must frame each other.
XFrameOptions string
// ReferrerPolicy sets the Referrer-Policy header.
// Recommended: "strict-origin-when-cross-origin".
ReferrerPolicy string
// ContentSecurityPolicy sets the Content-Security-Policy header.
// Empty by default because a CSP that breaks the application is
// worse than no CSP — applications should opt-in with a value
// tailored to their HTML/asset sources.
ContentSecurityPolicy string
// PermissionsPolicy sets the Permissions-Policy header (formerly
// Feature-Policy). Empty by default; set per application.
PermissionsPolicy string
// CrossOriginOpenerPolicy sets the Cross-Origin-Opener-Policy header.
// Recommended: "same-origin" for sites that need cross-origin
// isolation (e.g. SharedArrayBuffer).
CrossOriginOpenerPolicy string
// CrossOriginResourcePolicy sets the Cross-Origin-Resource-Policy header.
// Recommended: "same-origin" for API responses that should not be
// embedded by other origins.
CrossOriginResourcePolicy string
// HSTSMaxAgeSeconds, when positive, emits a Strict-Transport-Security
// header — but ONLY when the request was served over TLS
// (r.TLS != nil). Sending HSTS over plain HTTP is a no-op per RFC
// 6797 and risks confusing intermediaries; gating on TLS also
// prevents a misconfigured local-dev deployment from accidentally
// pinning HSTS for users.
//
// Common value: 31536000 (1 year).
HSTSMaxAgeSeconds int
// HSTSIncludeSubdomains adds the includeSubDomains directive.
HSTSIncludeSubdomains bool
// HSTSPreload adds the preload directive (only set this if you
// understand the implications: https://hstspreload.org/).
HSTSPreload bool
}
SecurityHeadersConfig controls which response headers SecurityHeadersMiddleware emits. The zero value is a sensible production default; create one with DefaultSecurityHeadersConfig() and tweak fields as needed.
All fields except HSTSMaxAgeSeconds are raw header values that are sent verbatim. Setting a field to the empty string disables the header entirely.
func DefaultSecurityHeadersConfig ¶ added in v1.6.0
func DefaultSecurityHeadersConfig() SecurityHeadersConfig
DefaultSecurityHeadersConfig returns a security-headers configuration with safe production defaults. CSP and Permissions-Policy are left empty because they require per-application tuning.
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 SessionRefreshResponse ¶ added in v1.5.0
type SessionRefreshResponse struct {
// ExpiresAt is when the new session expires
ExpiresAt string `json:"expiresAt"`
}
SessionRefreshResponse represents the data payload returned by the session refresh endpoint. This is wrapped in a core.Response envelope.
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, logger Logger) *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) AddBearerTokenValidator ¶ added in v1.5.1
func (s *SessionService) AddBearerTokenValidator(v BearerTokenValidator)
AddBearerTokenValidator appends a validator to the bearer token validation chain. Multiple plugins can each register their own validator; they are tried in registration order and the first to return a non-nil user wins.
func (*SessionService) CountUserSessions ¶ added in v1.5.0
CountUserSessions returns the total number of active sessions for a user
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 raw tokens are returned to the caller (for cookie or response use), but only their SHA-256 hashes are persisted to the database and to Redis. A DB read alone therefore cannot impersonate the user — an attacker would also need to compute a pre-image of the hash.
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 containing the **raw** secrets. These should be sent to the client (HTTP-only cookies or Authorization header) and then discarded server-side.
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. Accepts the **raw** session token from the client; the token is hashed before any store/cache access.
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) GetBearerTokenValidators ¶ added in v1.5.1
func (s *SessionService) GetBearerTokenValidators() []BearerTokenValidator
GetBearerTokenValidators returns a snapshot of the registered validators.
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, offset, limit int) ([]*auth.Session, error)
GetUserSessions retrieves a paginated list of 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) MigrateHashSessionTokensForUser ¶ added in v1.6.0
func (s *SessionService) MigrateHashSessionTokensForUser(ctx context.Context, userID string) (migrated int, err error)
MigrateHashSessionTokensForUser rewrites any plaintext session/refresh tokens in the underlying store as their SHA-256 hex hashes (the at-rest format used by CreateSession). It is idempotent: rows whose token columns already look like SHA-256 hex digests (see IsHashedToken) are skipped.
Run this once after upgrading to the hashed-at-rest scheme. After a successful run, all subsequent ValidateSession / DeleteSession / RefreshSession calls will continue to work because they hash inputs before querying the store. Plaintext tokens issued before the migration become invalid the moment they are rewritten — clients holding only those tokens must re-authenticate.
The walk uses GetByUserID per user, requiring callers to provide a userID iterator. We expose two entry points:
- MigrateHashSessionTokensForUser: rotates a single user's rows. Use this for incremental migrations or scripted one-offs.
A bulk migration across all users is intentionally not provided here: SessionStore does not expose a "list all session IDs" query, and adding one would expand the public store contract for a one-time operation. Callers that need a global migration should iterate user IDs from their UserStore and invoke MigrateHashSessionTokensForUser per user.
func (*SessionService) RefreshSession ¶
func (s *SessionService) RefreshSession(ctx context.Context, refreshToken string) (*auth.Session, error)
RefreshSession rotates an existing session using a refresh token.
Implementation note: this is a delete-old + create-new operation rather than an in-place update. That guarantees the access token rotates at rest (the existing UpdateSession SQL does not touch the token column) and keeps session-token rotation atomic with refresh-token rotation.
Accepts the **raw** refresh token from the client; the token is hashed before any store access. Returns a session with **raw** tokens in its Token/RefreshToken fields so the caller can hand them to the client.
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:
- Hash the input token (SHA-256)
- Check Redis cache if available (fast path, keyed by hash)
- 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 raw session token presented by the client
Returns:
- *auth.Session: The valid session. Token/RefreshToken on the returned struct hold their **hashed** values (the raw token presented by the client is not retained) — callers needing the raw token already have it as the input argument.
- *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": "user@example.com",
"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", "john@example.com", "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
Source Files
¶
- account.go
- audit.go
- auth.go
- config.go
- constants.go
- context.go
- cookies.go
- crypto.go
- csrf.go
- email_password.go
- errors.go
- http.go
- interfaces.go
- logger.go
- middleware.go
- openapi.go
- password.go
- ratelimit.go
- redis.go
- sanitization.go
- schema.go
- schema_dialects.go
- secrets.go
- security_headers.go
- session.go
- sql_identifier.go
- user.go
- utils.go
- validation.go
- verification.go