# Know Foolery - Detailed Security Implementation Guidelines
## Authentication & Authorization
### OAuth 2.0/OIDC Implementation
- See [Zitadel Integration Guidelines](zitadel-guidelines.md)
### Session Management
```go
// Secure session management
package session
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
type SessionManager struct {
redis *redis.Client
entropy int
maxAge time.Duration
secureCookie bool
}
type Session struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
LastSeen time.Time `json:"last_seen"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
DeviceID string `json:"device_id,omitempty"`
Roles []string `json:"roles"`
MFAVerified bool `json:"mfa_verified"`
}
func NewSessionManager(redis *redis.Client, secureCookie bool) *SessionManager {
return &SessionManager{
redis: redis,
entropy: 32, // 256 bits of entropy
maxAge: 24 * time.Hour,
secureCookie: secureCookie,
}
}
func (sm *SessionManager) CreateSession(ctx context.Context, userID, ipAddress, userAgent string, roles []string, mfaVerified bool) (*Session, error) {
// Generate cryptographically secure session ID
sessionID, err := sm.generateSecureSessionID()
if err != nil {
return nil, fmt.Errorf("failed to generate session ID: %w", err)
}
session := &Session{
ID: sessionID,
UserID: userID,
CreatedAt: time.Now(),
LastSeen: time.Now(),
IPAddress: ipAddress,
UserAgent: userAgent,
DeviceID: sm.generateDeviceFingerprint(ipAddress, userAgent),
Roles: roles,
MFAVerified: mfaVerified,
}
// Store session in Redis with expiration
sessionKey := fmt.Sprintf("session:%s", sessionID)
sessionData, err := json.Marshal(session)
if err != nil {
return nil, fmt.Errorf("failed to marshal session: %w", err)
}
err = sm.redis.SetEX(ctx, sessionKey, sessionData, sm.maxAge).Err()
if err != nil {
return nil, fmt.Errorf("failed to store session: %w", err)
}
// Store user session mapping for concurrent session limiting
userSessionKey := fmt.Sprintf("user_sessions:%s", userID)
sm.redis.SAdd(ctx, userSessionKey, sessionID)
sm.redis.Expire(ctx, userSessionKey, sm.maxAge)
return session, nil
}
func (sm *SessionManager) GetSession(ctx context.Context, sessionID string) (*Session, error) {
sessionKey := fmt.Sprintf("session:%s", sessionID)
sessionData, err := sm.redis.Get(ctx, sessionKey).Result()
if err == redis.Nil {
return nil, fmt.Errorf("session not found")
} else if err != nil {
return nil, fmt.Errorf("failed to retrieve session: %w", err)
}
var session Session
err = json.Unmarshal([]byte(sessionData), &session)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal session: %w", err)
}
return &session, nil
}
func (sm *SessionManager) IsValidSession(sessionID, userID string) bool {
ctx := context.Background()
session, err := sm.GetSession(ctx, sessionID)
if err != nil {
return false
}
// Check if session belongs to the correct user
if session.UserID != userID {
return false
}
// Update last seen timestamp
session.LastSeen = time.Now()
sm.updateSession(ctx, session)
return true
}
func (sm *SessionManager) InvalidateSession(ctx context.Context, sessionID string) error {
session, err := sm.GetSession(ctx, sessionID)
if err != nil {
return err
}
// Remove from Redis
sessionKey := fmt.Sprintf("session:%s", sessionID)
sm.redis.Del(ctx, sessionKey)
// Remove from user sessions set
userSessionKey := fmt.Sprintf("user_sessions:%s", session.UserID)
sm.redis.SRem(ctx, userSessionKey, sessionID)
return nil
}
func (sm *SessionManager) InvalidateAllUserSessions(ctx context.Context, userID string) error {
userSessionKey := fmt.Sprintf("user_sessions:%s", userID)
sessionIDs, err := sm.redis.SMembers(ctx, userSessionKey).Result()
if err != nil {
return err
}
// Remove all sessions
for _, sessionID := range sessionIDs {
sessionKey := fmt.Sprintf("session:%s", sessionID)
sm.redis.Del(ctx, sessionKey)
}
// Clear user sessions set
sm.redis.Del(ctx, userSessionKey)
return nil
}
func (sm *SessionManager) generateSecureSessionID() (string, error) {
bytes := make([]byte, sm.entropy)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
// Hash the random bytes for additional security
hash := sha256.Sum256(bytes)
return hex.EncodeToString(hash[:]), nil
}
func (sm *SessionManager) generateDeviceFingerprint(ipAddress, userAgent string) string {
data := fmt.Sprintf("%s:%s", ipAddress, userAgent)
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:16]) // 128-bit fingerprint
}
func (sm *SessionManager) updateSession(ctx context.Context, session *Session) error {
sessionKey := fmt.Sprintf("session:%s", session.ID)
sessionData, err := json.Marshal(session)
if err != nil {
return err
}
return sm.redis.SetEX(ctx, sessionKey, sessionData, sm.maxAge).Err()
}
```
## Input Validation & Sanitization
### Comprehensive Input Validation
```go
// Input validation and sanitization framework
package validation
import (
"fmt"
"html"
"regexp"
"strings"
"unicode"
"github.com/go-playground/validator/v10"
)
type InputValidator struct {
validator *validator.Validate
rules map[string]*ValidationRule
}
type ValidationRule struct {
MaxLength int
MinLength int
Pattern *regexp.Regexp
AllowedChars *regexp.Regexp
Sanitizer func(string) string
}
func NewInputValidator() *InputValidator {
v := validator.New()
// Register custom validations
v.RegisterValidation("alphanum_space", validateAlphanumSpace)
v.RegisterValidation("no_html", validateNoHTML)
v.RegisterValidation("safe_text", validateSafeText)
iv := &InputValidator{
validator: v,
rules: make(map[string]*ValidationRule),
}
iv.setupValidationRules()
return iv
}
func (iv *InputValidator) setupValidationRules() {
// Player name validation
iv.rules["player_name"] = &ValidationRule{
MaxLength: 50,
MinLength: 2,
AllowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`),
Sanitizer: iv.sanitizePlayerName,
}
// Answer validation
iv.rules["answer"] = &ValidationRule{
MaxLength: 500,
MinLength: 1,
AllowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.,'!?()]+$`),
Sanitizer: iv.sanitizeAnswer,
}
// Question text validation (admin only)
iv.rules["question_text"] = &ValidationRule{
MaxLength: 1000,
MinLength: 10,
Sanitizer: iv.sanitizeQuestionText,
}
// Theme validation
iv.rules["theme"] = &ValidationRule{
MaxLength: 100,
MinLength: 2,
AllowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`),
Sanitizer: iv.sanitizeTheme,
}
}
// Validate and sanitize input based on field type
func (iv *InputValidator) ValidateAndSanitize(fieldType, input string) (string, error) {
rule, exists := iv.rules[fieldType]
if !exists {
return "", fmt.Errorf("unknown field type: %s", fieldType)
}
// Basic length validation
if len(input) < rule.MinLength {
return "", fmt.Errorf("input too short: minimum %d characters", rule.MinLength)
}
if len(input) > rule.MaxLength {
return "", fmt.Errorf("input too long: maximum %d characters", rule.MaxLength)
}
// Character validation
if rule.AllowedChars != nil && !rule.AllowedChars.MatchString(input) {
return "", fmt.Errorf("input contains invalid characters")
}
// Sanitize input
sanitized := input
if rule.Sanitizer != nil {
sanitized = rule.Sanitizer(input)
}
return sanitized, nil
}
// Sanitization functions
func (iv *InputValidator) sanitizePlayerName(input string) string {
// Remove HTML entities and tags
sanitized := html.EscapeString(input)
// Trim whitespace
sanitized = strings.TrimSpace(sanitized)
// Remove multiple consecutive spaces
spaceRegex := regexp.MustCompile(`\s+`)
sanitized = spaceRegex.ReplaceAllString(sanitized, " ")
return sanitized
}
func (iv *InputValidator) sanitizeAnswer(input string) string {
// HTML escape
sanitized := html.EscapeString(input)
// Trim and normalize whitespace
sanitized = strings.TrimSpace(sanitized)
// Convert to lowercase for comparison
sanitized = strings.ToLower(sanitized)
// Remove extra punctuation but keep essential ones
punctRegex := regexp.MustCompile(`[^\w\s\-'.]`)
sanitized = punctRegex.ReplaceAllString(sanitized, "")
return sanitized
}
func (iv *InputValidator) sanitizeQuestionText(input string) string {
// More permissive sanitization for question text
sanitized := html.EscapeString(input)
sanitized = strings.TrimSpace(sanitized)
// Remove potential script content
scriptRegex := regexp.MustCompile(`(?i)`)
sanitized = scriptRegex.ReplaceAllString(sanitized, "")
return sanitized
}
func (iv *InputValidator) sanitizeTheme(input string) string {
sanitized := html.EscapeString(input)
sanitized = strings.TrimSpace(sanitized)
// Capitalize first letter of each word
words := strings.Fields(sanitized)
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:])
}
}
return strings.Join(words, " ")
}
// Custom validation functions
func validateAlphanumSpace(fl validator.FieldLevel) bool {
str := fl.Field().String()
for _, r := range str {
if !unicode.IsLetter(r) && !unicode.IsNumber(r) && !unicode.IsSpace(r) && r != '-' && r != '_' && r != '.' {
return false
}
}
return true
}
func validateNoHTML(fl validator.FieldLevel) bool {
str := fl.Field().String()
return !strings.Contains(str, "<") && !strings.Contains(str, ">")
}
func validateSafeText(fl validator.FieldLevel) bool {
str := fl.Field().String()
// Check for potential XSS patterns
dangerousPatterns := []string{
"javascript:",
"data:",
"vbscript:",
"on\\w+\\s*=",
"