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 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 IsAuthError(err error) bool
- func IsEncrypted(value string) bool
- func MaxBodySizeMiddleware(maxBytes int64) func(http.Handler) http.Handler
- func OpenWithKey(key []byte, value string) (string, error)
- func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler
- func ReadJSON(r *http.Request, dst any) error
- func RequireAuthMiddleware(_ *SessionService) func(http.Handler) http.Handler
- func SanitizeEmail(email string) string
- func SanitizeMultiline(input string, maxLength int) string
- func SanitizePhoneNumber(phone 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 func() string)
- func SetHTTPLogger(l HTTPLogger)
- func SetIDStrategy(strategy IDStrategy)
- func SetPluginValue(ctx context.Context, key string, value any)
- func UserIDOf(user any) string
- func ValidateEmail(email string) 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 WriteJSON(w http.ResponseWriter, statusCode int, data any)
- func WriteJSONError(w http.ResponseWriter, statusCode int, message string)
- type AccountService
- func (s *AccountService) ConfirmPasswordReset(ctx context.Context, token, newPassword string) (auth.User, error)
- 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) GetAccountByProvider(ctx context.Context, provider, providerAccountID 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) RequestPasswordReset(ctx context.Context, email string) (auth.User, string, 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 AuditEvent
- type AuditEventType
- type AuditLogger
- type AuditSink
- type AuthConfig
- type AuthError
- type AuthService
- func (as *AuthService) AddAuditSink(sink AuditSink)
- func (as *AuthService) AuditLogger() AuditLogger
- func (as *AuthService) GetUserFieldsConfig() *UserFieldsConfig
- func (as *AuthService) SetEmailVerificationCheck(fn func(ctx context.Context, user auth.User) (bool, error))
- func (as *AuthService) SetEmailVerificationResetter(fn func(ctx context.Context, userID, email string) error)
- func (as *AuthService) SetEmailVerifiedMarker(fn func(ctx context.Context, userID, email string) error)
- func (as *AuthService) ValidatePassword(password string) error
- 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
- func (h *EmailPasswordHandlers) Login(ctx context.Context, identifier, password string) (*LoginResult, error)
- func (h *EmailPasswordHandlers) Register(ctx context.Context, name, email, password string) (*RegisterResult, error)
- func (h *EmailPasswordHandlers) RegisterWithUsername(ctx context.Context, name, email, username, password string) (*RegisterResult, error)
- type EnrichedUser
- func (eu *EnrichedUser) Get(key string) any
- func (eu *EnrichedUser) GetAs[T any](key string) (T, bool)
- 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 IDStrategy
- type KeyManager
- type Logger
- 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) GetAs[T any](key string) (T, bool)
- 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) Configure(cfg *SessionConfig)
- 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) ConfirmEmailChange(ctx context.Context, userID, token string) (auth.User, error)
- 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) CreateUserWithUsername(ctx context.Context, name, email, username, 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) RequestEmailChange(ctx context.Context, userID, newEmail string, customToken *string) (string, 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 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)
- func (s *VerificationService) ValidateVerificationFor(ctx context.Context, identifier, vType, 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 ( // DefaultPaginationLimit is the default number of items per page DefaultPaginationLimit = 20 // MaxPaginationLimit is the maximum allowed items per page MaxPaginationLimit = 100 )
Default pagination limits for list 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 ( // DefaultPasswordResetExpiry is the default lifetime of a password-reset // token (1 hour). Short enough to limit the value of a leaked mail. DefaultPasswordResetExpiry = time.Hour // DefaultEmailChangeExpiry is the default lifetime of an email-change // confirmation token (1 hour). DefaultEmailChangeExpiry = time.Hour )
Default account-security flow 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 ( // VerificationTypeEmailVerification is used by email-verification OTPs. VerificationTypeEmailVerification = "email_verification" // VerificationTypePasswordReset is used by password-reset tokens. VerificationTypePasswordReset = "password_reset" // VerificationTypeEmailChange is used by email-change confirmation tokens. VerificationTypeEmailChange = "email_change" )
Verification token types used by the account-security flows. These are the values persisted in verification.type: keep them stable, because tokens issued under one spelling cannot be validated under another.
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 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") // ErrEmailAlreadyExists indicates a user with the given email already exists ErrEmailAlreadyExists = errors.New("email already registered") // ErrUsernameTaken indicates the requested username is already in use ErrUsernameTaken = errors.New("username already taken") // 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 CSRFMiddleware ¶
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 converts an int to int32, clamping to the valid int32 range. Prevents overflow on platforms where int is larger than 32 bits and guards against untrusted values.
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. Defaults to ULID (26 chars, sortable by creation time); switch with SetIDStrategy / SetCustomIDGenerator before generating IDs.
func GenerateOTPCode ¶
GenerateOTPCode generates a random numeric OTP code of the given length using cryptographically secure randomness, with leading zeros preserved.
code, _ := core.GenerateOTPCode(6) // "042816", "912345", …
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 ¶
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 IsEncrypted ¶
IsEncrypted reports whether value was produced by SealWithDerivedKey.
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 OpenWithKey ¶
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 ReadJSON ¶
ReadJSON parses a JSON request body into dst using encoding/json/v2.
v2 defaults reject duplicate object names and invalid UTF-8 — both are malformed input that the v1 Decoder silently accepted. Case-insensitive name matching is enabled to preserve v1 client compatibility.
Example:
var req LoginRequest
if err := core.ReadJSON(r, &req); err != nil { ... }
func RequireAuthMiddleware ¶
func RequireAuthMiddleware(_ *SessionService) func(http.Handler) http.Handler
RequireAuthMiddleware returns middleware that requires authentication.
func SanitizeEmail ¶
SanitizeEmail normalizes an email address: strips whitespace, lowercases (emails are case-insensitive), and removes null/control bytes. It does NOT validate format — use ValidateEmail for that.
func SanitizeMultiline ¶
SanitizeMultiline sanitizes multi-line text (text areas, descriptions): strips scripts/HTML, normalizes line endings to \n, keeps newlines/tabs, and caps at maxLength (0 = default 5000).
func SanitizePhoneNumber ¶
SanitizePhoneNumber sanitizes phone numbers to digits, plus sign, and hyphens only:
core.SanitizePhoneNumber("+1 (555) 123-4567") // "+1555123-4567"
func SanitizeString ¶
func SanitizeString(input string, config *SanitizationConfig) string
SanitizeString is the primary sanitization function for user inputs like names and descriptions: it removes null bytes and control characters, optionally unescapes/strips HTML, normalizes whitespace, and enforces MaxLength (rune-aware). config nil = DefaultSanitizationConfig.
func SanitizeURL ¶
SanitizeURL trims whitespace, removes null/control bytes, and returns "" for dangerous schemes (javascript:, data:, vbscript:, file:, about:).
func SanitizeUsername ¶
SanitizeUsername sanitizes usernames: keeps only [a-z0-9_.-], lowercases, and trims to maxLength (0 = default of 50).
func SealWithKey ¶
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 ¶
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 func() string)
SetCustomIDGenerator sets a custom ID generation function and switches the strategy to IDStrategyCustom. The generator must return unique IDs and be thread-safe if called concurrently.
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.
Call during application initialization, before any IDs are generated; changing it later produces inconsistent ID formats.
func SetPluginValue ¶
SetPluginValue is a convenience function to set a plugin value directly in context. Does nothing if plugin data is not initialized.
func UserIDOf ¶
UserIDOf extracts a user ID from the two concrete shapes a context user can take (a UserModel implementation or *auth.User). Single owner of that type switch. Returns "" when user is neither.
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 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 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 (*AccountService) ConfirmPasswordReset ¶ added in v2.1.0
func (s *AccountService) ConfirmPasswordReset(ctx context.Context, token, newPassword string) (auth.User, error)
ConfirmPasswordReset redeems a password-reset token and sets the account's new password. UpdatePassword invalidates every existing session, so any attacker holding a stolen session loses access once the reset completes.
The token is consumed only after the password is updated: a rejected new password (policy, unknown token) leaves the token redeemable, so a user who mistypes a password does not have to restart the flow. The reverse risk — a DB failure after the update but before the token delete — is surfaced as an internal error rather than silently swallowed.
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) GetAccountByProvider ¶
func (s *AccountService) GetAccountByProvider(ctx context.Context, provider, providerAccountID string) (auth.Account, error)
GetAccountByProvider retrieves an account by provider name and provider account ID (e.g. ("credentials", username)).
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) RequestPasswordReset ¶ added in v2.1.0
func (s *AccountService) RequestPasswordReset(ctx context.Context, email string) (auth.User, string, error)
RequestPasswordReset issues a single-use password-reset token for the account that owns email and returns the raw token for delivery.
The token is stored hashed; only the value returned here can be redeemed through ConfirmPasswordReset. Any previously issued reset token for the same address is invalidated first, so at most one reset is in flight per account.
Returns ErrUserNotFound when no user owns that address. HTTP layers that must not reveal whether an account exists should treat that error as success and return the same response as a delivered reset.
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 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 AuditSink ¶ added in v2.1.0
type AuditSink interface {
OnAuthEvent(ctx context.Context, event *AuditEvent)
}
AuditSink receives a copy of every audit event in addition to the configured AuditLogger. Sinks are called after the primary logger and must not block; buffer internally if the sink is slow.
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
// RequireEmailVerification, when true, withholds the auto-login session on
// registration and refuses login until the email is verified. Verification
// requires an email plugin (e.g. email-otp) to register a checker via
// AuthService.SetEmailVerificationCheck; without one, registration still
// withholds the session and no user can log in, so only enable this when a
// checker is wired.
RequireEmailVerification bool
// PasswordResetExpiry is how long password-reset tokens remain valid.
// Zero uses DefaultPasswordResetExpiry (1 hour).
PasswordResetExpiry time.Duration
// EmailChangeExpiry is how long email-change confirmation tokens remain
// valid. Zero uses DefaultEmailChangeExpiry (1 hour).
EmailChangeExpiry time.Duration
// 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) AddAuditSink ¶ added in v2.1.0
func (as *AuthService) AddAuditSink(sink AuditSink)
AddAuditSink registers a sink that receives a copy of every audit event. Plugins typically call this during Init. Safe to call before any event is emitted; the fan-out is created on first use.
func (*AuthService) AuditLogger ¶ added in v2.1.0
func (as *AuthService) AuditLogger() AuditLogger
AuditLogger returns the configured audit logger so plugins can emit their own security events through the same pipeline.
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.
func (*AuthService) SetEmailVerificationCheck ¶
func (as *AuthService) SetEmailVerificationCheck(fn func(ctx context.Context, user auth.User) (bool, error))
SetEmailVerificationCheck wires the email-verification checker used when AuthConfig.RequireEmailVerification is enabled. Email plugins call this during Init. Passing nil disables the check.
func (*AuthService) SetEmailVerificationResetter ¶
func (as *AuthService) SetEmailVerificationResetter(fn func(ctx context.Context, userID, email string) error)
SetEmailVerificationResetter wires the callback invoked when a user's email changes, so the email plugin can clear its verification flag. Email plugins call this during Init. Passing nil disables the reset.
func (*AuthService) SetEmailVerifiedMarker ¶ added in v2.1.0
func (as *AuthService) SetEmailVerifiedMarker(fn func(ctx context.Context, userID, email string) error)
SetEmailVerifiedMarker wires the callback invoked after a confirmed email change, so the email plugin can mark the new address verified. Email plugins call this during Init. Passing nil disables the marker.
func (*AuthService) ValidatePassword ¶
func (as *AuthService) ValidatePassword(password string) error
ValidatePassword checks password against the configured password policy. Exposed so plugins and applications that create credentials outside UserService enforce the same policy.
type BearerTokenValidator ¶
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 ¶
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 ¶
func DefaultCSRFConfig() CSRFConfig
DefaultCSRFConfig returns a CSRFConfig populated with safe defaults. MasterSecret must still be set by the caller.
type ColumnSpec ¶
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, applying SessionConfig.CookieSettings security defaults (HTTPOnly, Secure, SameSite, Domain, Path) to every cookie operation.
func NewCookieManager ¶
func NewCookieManager(cfg *SessionConfig) *CookieManager
NewCookieManager creates a new CookieManager with the given configuration. If cfg is nil, uses DefaultSessionConfig().
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.
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 (Domain, Secure, HTTPOnly, SameSite from config; Path always "/").
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 (Login / Register).
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, identifier, password string) (*LoginResult, error)
Login authenticates a user with a password. identifier may be either the user's email address or their username (when one was set at registration). 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.
func (*EmailPasswordHandlers) RegisterWithUsername ¶
func (h *EmailPasswordHandlers) RegisterWithUsername(ctx context.Context, name, email, username, password string) (*RegisterResult, error)
RegisterWithUsername registers a user with an optional unique username that can also be used to log in. When AuthConfig.RequireEmailVerification is set, no session is issued; the caller must complete email verification first.
type EnrichedUser ¶
type EnrichedUser struct {
*auth.User
Extensions map[string]any `json:"-"` // flattened into responses by MarshalJSON
// contains filtered or unexported fields
}
EnrichedUser wraps the core auth.User with plugin-specific extensions.
Plugins augment the base user with extra fields (stored in Extensions) that are flattened as top-level fields in JSON responses — e.g. the admin plugin adds "role", the organizations plugin adds "organizations". Keys are simple field names; all methods are safe for concurrent use via an 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 NewEnrichedUser ¶
func NewEnrichedUser(user *auth.User) *EnrichedUser
NewEnrichedUser creates an EnrichedUser wrapping a core User.
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 (GetBool, GetAs, etc.).
func (*EnrichedUser) GetAs ¶
func (eu *EnrichedUser) GetAs[T any](key string) (T, bool)
GetAs retrieves an extension value asserting it to T. This is the type-safe primitive behind GetString/GetBool/GetStringSlice/GetMap; prefer it for any extension type without a dedicated getter.
Returns the zero value of T and false when the key is missing or the value is not a T. (Requires Go 1.27 generic methods.)
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. Key becomes a top-level field in JSON responses.
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 IDStrategy ¶
type IDStrategy string
IDStrategy defines the algorithm used for generating unique identifiers.
const ( // IDStrategyULID uses ULID (Universally Unique Lexicographically Sortable // Identifier) — the default. Sortable by creation time, compact (26 // characters), database-index friendly. IDStrategyULID IDStrategy = "ulid" // IDStrategyUUID uses UUID v4 (random UUIDs). Standard format, maximum // randomness, but not sortable. IDStrategyUUID IDStrategy = "uuid" // IDStrategyCustom uses a user-provided custom ID generation function // (KSUID, Snowflake, nanoid, database sequences, …). Set the generator // with core.SetCustomIDGenerator. IDStrategyCustom IDStrategy = "custom" )
func GetIDStrategy ¶
func GetIDStrategy() IDStrategy
GetIDStrategy returns the currently active ID generation strategy.
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 used by plugins for temporary data (OAuth state tokens, CSRF tokens, verification codes, JWT blacklists). Implementations: StaticKeyManager (in-memory, development/testing) and RedisKeyManager (production).
type Logger ¶
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 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 ¶
type LoginRequest struct {
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"`
}
LoginRequest represents the JSON payload for email+password login. Provide either Email or Username (Email is tried first when both are set).
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 ¶
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.
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.
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) GetAs ¶
func (pd *PluginData) GetAs[T any](key string) (T, bool)
GetAs retrieves a value asserting it to T (generic-method twin of EnrichedUser.GetAs). Returns the zero value of T and false when the key is missing or the value is not a T.
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 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 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"`
Username string `json:"username,omitempty"`
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). Nil when
// VerificationRequired is true.
Session *auth.Session
// Token is the session token. Empty when VerificationRequired is true.
Token string
// VerificationRequired reports that the account was created but no
// session was issued because email verification is required first.
VerificationRequired bool
}
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 int // maximum length (0 = no limit)
AllowUnicode bool // permits non-ASCII Unicode characters
StripHTML bool // removes all HTML tags from input
// NormalizeWhitespace collapses runs of whitespace into single spaces.
NormalizeWhitespace bool
TrimWhitespace bool // removes leading/trailing whitespace
}
SanitizationConfig controls the behavior of sanitization functions. Sanitization is NOT a replacement for validation, parameterized queries, or output encoding — it is one layer of defense.
func DefaultSanitizationConfig ¶
func DefaultSanitizationConfig() *SanitizationConfig
DefaultSanitizationConfig returns a secure default configuration (1000-char limit, Unicode allowed, HTML stripped, whitespace normalized).
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 (*SessionService) AddBearerTokenValidator ¶
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) Configure ¶
func (s *SessionService) Configure(cfg *SessionConfig)
Configure replaces the session configuration and Redis client. Intended to be called once at startup (before any traffic) when the service was built without a config, e.g. via NewAuthService. Rebuilds the cookie manager so cookie settings take effect.
func (*SessionService) CountUserSessions ¶
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.
IP address and user agent are extracted from the request context (populated by AegisContextMiddleware). Logs a successful login audit event.
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 and purges every Redis cache entry for those sessions. Without the cache purge, revoked sessions would keep authenticating from Redis until their cached TTL expires (ValidateSession prefers the cache over the database).
func (*SessionService) EnableBearerAuth ¶
func (s *SessionService) EnableBearerAuth()
EnableBearerAuth enables Bearer token authentication for sessions.
func (*SessionService) GetBearerTokenValidators ¶
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 ¶
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, verifying ownership first.
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.
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 for development and testing: data is lost on restart, is not shared across instances, and expiry is NOT supported. Use RedisKeyManager in production.
func NewStaticKeyManager ¶
func NewStaticKeyManager() (*StaticKeyManager, error)
NewStaticKeyManager creates a new in-memory key manager.
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 (*UserService) ConfirmEmailChange ¶ added in v2.1.0
func (s *UserService) ConfirmEmailChange(ctx context.Context, userID, token string) (auth.User, error)
ConfirmEmailChange redeems an email-change token and moves the account's email. userID is the authenticated caller whose account changes; the token itself is scoped to the new address, so possession of it proves control of that mailbox.
UpdateUserEmail clears the plugin-side verification flag for the new address before switching; because the caller just proved control of the address, the verified marker (when an email plugin wired one) is set again afterwards.
The token is consumed only after the change is applied, so a failed uniqueness/validation check leaves it redeemable.
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 validates the password against the configured policy, persists the user, hashes the password with the configured Argon2id parameters, and creates a password-based account (provider="credentials") linked to the user.
The user and account are created atomically: in one transaction when the store supports it, otherwise the user is deleted if account creation fails. Returns ErrEmailAlreadyExists if the email is taken.
func (*UserService) CreateUserWithEmail ¶
func (s *UserService) CreateUserWithEmail(ctx context.Context, name, email, password string) (auth.User, error)
CreateUserWithEmail creates a user with email/password from individual fields, delegating to CreateUser.
func (*UserService) CreateUserWithUsername ¶
func (s *UserService) CreateUserWithUsername(ctx context.Context, name, email, username, password string) (auth.User, error)
CreateUserWithUsername creates a user whose credentials account is also addressable by a unique username (stored as the account's provider account ID). Login accepts either the email or the username.
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.
Used for OAuth-only users, admin-created users, or service accounts. The user won't be able to log in with email/password until a password account is created separately.
func (*UserService) DeleteUser ¶
func (s *UserService) DeleteUser(ctx context.Context, id string) error
DeleteUser deletes a user and all associated data (accounts and sessions).
When the store supports transactions, the session/account/user deletes run atomically. Otherwise they run in sequence (sessions first to satisfy foreign keys). Cached sessions are purged first, best-effort, so Redis cannot serve a deleted user until TTL.
func (*UserService) GetUserByEmail ¶
GetUserByEmail retrieves a user by their email address.
func (*UserService) GetUserByID ¶
GetUserByID retrieves a user by their unique ID.
func (*UserService) RequestEmailChange ¶ added in v2.1.0
func (s *UserService) RequestEmailChange(ctx context.Context, userID, newEmail string, customToken *string) (string, error)
RequestEmailChange issues a confirmation token for moving userID's account to newEmail and returns the raw token for delivery to that address.
customToken lets the caller supply a short code (e.g. a 6-digit OTP) instead of the generated 64-character token; it is stored hashed like any other verification token. Pass nil for the default token.
The token is stored hashed and scoped to the new address (identifier = newEmail, type = email_change), so redeeming it proves control of that mailbox. The account keeps its current address until ConfirmEmailChange is called. Any previously issued change token for the same address is invalidated first.
Returns ErrEmailAlreadyExists when another account already owns newEmail.
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 changes a user's email address.
The new address is validated and checked for uniqueness. Any verification flag is cleared first (via the wired email plugin) so a previously-verified address cannot make the new one look verified. Returns ErrEmailAlreadyExists if another account owns the address.
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 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 (*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.
Tokens are stored at rest as their SHA-256 hex hash (same scheme as session tokens) — a database read alone cannot impersonate a user. The returned verification carries the **raw** token so callers can deliver it to the user.
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.
Note: tokens issued before the hashing scheme are not matched anymore; verification tokens are short-lived, so a deploy simply invalidates any pending ones.
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. Its Token field holds the stored **hash**, not the raw token the caller passed in.
- AuthErrorCodeTokenExpired if expired
- Error if token not found
func (*VerificationService) ValidateVerificationFor ¶
func (s *VerificationService) ValidateVerificationFor(ctx context.Context, identifier, vType, token string) (auth.Verification, error)
ValidateVerificationFor validates a token scoped to a specific identifier and verification type, then consumes (deletes) it so it cannot be reused.
This is the correct entry point for one-time codes (OTPs): binding to the identifier prevents a code sent to one email/phone from verifying another, binding to the type prevents cross-purpose reuse, and deletion enforces single use.
Returns AuthErrorCodeTokenExpired for expired tokens, an invalid-credentials error if the token does not match the identifier/type, or the underlying store error if not found.
Source Files
¶
- account.go
- audit.go
- auth.go
- config.go
- constants.go
- context.go
- cookies.go
- crypto.go
- csrf.go
- email_change.go
- email_password.go
- errors.go
- http.go
- interfaces.go
- logger.go
- middleware.go
- openapi.go
- password.go
- password_reset.go
- ratelimit.go
- redis.go
- sanitization.go
- schema.go
- schema_dialects.go
- secrets.go
- security.go
- security_headers.go
- session.go
- sql_identifier.go
- user.go
- utils.go
- validation.go
- verification.go