Documentation

Security

Overview

Security is a critical aspect of web application development. The framework provides built-in security features and patterns to protect against common vulnerabilities.

Authentication & Authorization

JWT Authentication

type AuthService struct {
    secretKey []byte
    userFetcher *srv.UserFetcher
}

func NewAuthService(secretKey string, userFetcher *srv.UserFetcher) *AuthService {
    return &AuthService{
        secretKey: []byte(secretKey),
        userFetcher: userFetcher,
    }
}

func (as *AuthService) GenerateToken(user *mdl.User) (string, error) {
    claims := jwt.MapClaims{
        "user_id": user.Id,
        "email":   user.Email,
        "exp":     time.Now().Add(time.Hour * 24).Unix(),
        "iat":     time.Now().Unix(),
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(as.secretKey)
}

func (as *AuthService) ValidateToken(tokenString string) (*mdl.User, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return as.secretKey, nil
    })

    if err != nil || !token.Valid {
        return nil, errors.New("invalid token")
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, errors.New("invalid token claims")
    }

    userID := claims["user_id"].(string)
    user, exists, err := as.userFetcher.FindOneById(context.Background(), userID, nil)
    if err != nil || !exists {
        return nil, errors.New("user not found")
    }

    return user, nil
}

Password Hashing

type PasswordService struct {
    cost int
}

func NewPasswordService() *PasswordService {
    return &PasswordService{cost: bcrypt.DefaultCost}
}

func (ps *PasswordService) HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), ps.cost)
    return string(bytes), err
}

func (ps *PasswordService) CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func (ps *PasswordService) ValidatePasswordStrength(password string) error {
    if len(password) < 8 {
        return errors.New("password must be at least 8 characters long")
    }

    hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
    hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
    hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
    hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)

    if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
        return errors.New("password must contain uppercase, lowercase, number, and special character")
    }

    return nil
}

Role-Based Access Control

type Permission string

const (
    PermissionCreateGarden Permission = "garden:create"
    PermissionReadGarden   Permission = "garden:read"
    PermissionUpdateGarden Permission = "garden:update"
    PermissionDeleteGarden Permission = "garden:delete"
    PermissionManageUsers  Permission = "user:manage"
)

type Role struct {
    Name        string
    Permissions []Permission
}

var Roles = map[string]Role{
    "admin": {
        Name: "admin",
        Permissions: []Permission{
            PermissionCreateGarden,
            PermissionReadGarden,
            PermissionUpdateGarden,
            PermissionDeleteGarden,
            PermissionManageUsers,
        },
    },
    "user": {
        Name: "user",
        Permissions: []Permission{
            PermissionCreateGarden,
            PermissionReadGarden,
            PermissionUpdateGarden,
        },
    },
    "viewer": {
        Name: "viewer",
        Permissions: []Permission{
            PermissionReadGarden,
        },
    },
}

func (u *mdl.User) HasPermission(permission Permission) bool {
    role, exists := Roles[u.Role]
    if !exists {
        return false
    }

    for _, p := range role.Permissions {
        if p == permission {
            return true
        }
    }
    return false
}

// Middleware for permission checking
func RequirePermission(permission Permission) gin.HandlerFunc {
    return func(c *gin.Context) {
        user, exists := c.Get("current_user")
        if !exists {
            c.JSON(401, gin.H{"error": "Authentication required"})
            c.Abort()
            return
        }

        if !user.(*mdl.User).HasPermission(permission) {
            c.JSON(403, gin.H{"error": "Insufficient permissions"})
            c.Abort()
            return
        }

        c.Next()
    }
}

Input Validation & Sanitization

SQL Injection Prevention

// Always use parameterized queries
func (f *GardenFetcher) FindByName(ctx context.Context, name string) ([]*mdl.Garden, error) {
    // ✅ Correct - parameterized query
    query := "SELECT * FROM gardens WHERE name = ?"
    rows, err := f.db.QueryContext(ctx, query, name)

    // ❌ Never do this - SQL injection vulnerability
    // query := fmt.Sprintf("SELECT * FROM gardens WHERE name = '%s'", name)
    // rows, err := f.db.QueryContext(ctx, query)

    if err != nil {
        return nil, err
    }
    defer rows.Close()

    return f.scanRows(rows)
}

// For dynamic WHERE clauses, use query builders
func (f *GardenFetcher) FindWithFilters(ctx context.Context, filters map[string]interface{}) ([]*mdl.Garden, error) {
    query := "SELECT * FROM gardens WHERE 1=1"
    args := make([]interface{}, 0)

    for field, value := range filters {
        switch field {
        case "name":
            query += " AND name = ?"
            args = append(args, value)
        case "active":
            query += " AND active = ?"
            args = append(args, value)
        case "user_id":
            query += " AND user_id = ?"
            args = append(args, value)
        }
    }

    rows, err := f.db.QueryContext(ctx, query, args...)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    return f.scanRows(rows)
}

XSS Prevention

import "html"

// HTML escaping for templates
func EscapeHTML(input string) string {
    return html.EscapeString(input)
}

// Custom template function for safe HTML
func SetupTemplates() *gin.Engine {
    router := gin.Default()

    router.SetFuncMap(template.FuncMap{
        "escape": EscapeHTML,
        "safeHTML": func(input string) template.HTML {
            // Only use for trusted content
            return template.HTML(input)
        },
    })

    return router
}

// Content Security Policy middleware
func CSPMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        csp := "default-src 'self'; " +
               "script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; " +
               "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
               "font-src 'self' https://fonts.gstatic.com; " +
               "img-src 'self' data: https:; " +
               "connect-src 'self'"

        c.Header("Content-Security-Policy", csp)
        c.Next()
    }
}

Input Sanitization

import (
    "regexp"
    "strings"
    "unicode"
)

type Sanitizer struct {
    htmlTagRegex  *regexp.Regexp
    scriptRegex   *regexp.Regexp
    sqlKeywords   []string
}

func NewSanitizer() *Sanitizer {
    return &Sanitizer{
        htmlTagRegex: regexp.MustCompile(`<[^>]*>`),
        scriptRegex:  regexp.MustCompile(`(?i)script`),
        sqlKeywords:  []string{"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "UNION"},
    }
}

func (s *Sanitizer) SanitizeString(input string) string {
    // Remove HTML tags
    cleaned := s.htmlTagRegex.ReplaceAllString(input, "")

    // Remove script content
    cleaned = s.scriptRegex.ReplaceAllString(cleaned, "")

    // Trim whitespace
    cleaned = strings.TrimSpace(cleaned)

    return cleaned
}

func (s *Sanitizer) ValidateNoSQLInjection(input string) error {
    upperInput := strings.ToUpper(input)

    for _, keyword := range s.sqlKeywords {
        if strings.Contains(upperInput, keyword) {
            return errors.New("potentially harmful SQL keyword detected")
        }
    }

    return nil
}

func (s *Sanitizer) SanitizeFilename(filename string) string {
    // Remove path separators and dangerous characters
    cleaned := regexp.MustCompile(`[<>:"/\\|?*]`).ReplaceAllString(filename, "")

    // Remove leading dots and spaces
    cleaned = strings.TrimLeftFunc(cleaned, func(r rune) bool {
        return r == '.' || unicode.IsSpace(r)
    })

    return cleaned
}

CSRF Protection

CSRF Token Implementation

type CSRFService struct {
    secretKey []byte
}

func NewCSRFService(secretKey string) *CSRFService {
    return &CSRFService{secretKey: []byte(secretKey)}
}

func (cs *CSRFService) GenerateToken(sessionID string) string {
    h := hmac.New(sha256.New, cs.secretKey)
    h.Write([]byte(sessionID))
    h.Write([]byte(time.Now().Format("2006-01-02")))

    return base64.URLEncoding.EncodeToString(h.Sum(nil))
}

func (cs *CSRFService) ValidateToken(token, sessionID string) bool {
    expectedToken := cs.GenerateToken(sessionID)
    return hmac.Equal([]byte(token), []byte(expectedToken))
}

// CSRF middleware
func CSRFMiddleware(csrfService *CSRFService) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Skip GET, HEAD, OPTIONS requests
        if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
            c.Next()
            return
        }

        sessionID := c.GetString("session_id")
        if sessionID == "" {
            c.JSON(403, gin.H{"error": "Session required"})
            c.Abort()
            return
        }

        token := c.GetHeader("X-CSRF-Token")
        if token == "" {
            token = c.PostForm("_token")
        }

        if !csrfService.ValidateToken(token, sessionID) {
            c.JSON(403, gin.H{"error": "Invalid CSRF token"})
            c.Abort()
            return
        }

        c.Next()
    }
}

Rate Limiting

Request Rate Limiting

import "golang.org/x/time/rate"

type RateLimiter struct {
    limiters map[string]*rate.Limiter
    mutex    sync.RWMutex
    rate     rate.Limit
    burst    int
}

func NewRateLimiter(requests int, window time.Duration) *RateLimiter {
    return &RateLimiter{
        limiters: make(map[string]*rate.Limiter),
        rate:     rate.Every(window / time.Duration(requests)),
        burst:    requests,
    }
}

func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
    rl.mutex.Lock()
    defer rl.mutex.Unlock()

    limiter, exists := rl.limiters[key]
    if !exists {
        limiter = rate.NewLimiter(rl.rate, rl.burst)
        rl.limiters[key] = limiter
    }

    return limiter
}

func (rl *RateLimiter) Allow(key string) bool {
    limiter := rl.GetLimiter(key)
    return limiter.Allow()
}

// Rate limiting middleware
func RateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        key := c.ClientIP()

        if !limiter.Allow(key) {
            c.JSON(429, gin.H{
                "error": "Rate limit exceeded",
                "retry_after": "60s",
            })
            c.Abort()
            return
        }

        c.Next()
    }
}

File Upload Security

Secure File Uploads

type FileUploadService struct {
    allowedExtensions []string
    allowedMimeTypes  []string
    maxFileSize       int64
    uploadDir         string
    sanitizer         *Sanitizer
}

func NewFileUploadService(uploadDir string) *FileUploadService {
    return &FileUploadService{
        allowedExtensions: []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"},
        allowedMimeTypes:  []string{"image/jpeg", "image/png", "image/gif", "application/pdf"},
        maxFileSize:       10 << 20, // 10MB
        uploadDir:         uploadDir,
        sanitizer:         NewSanitizer(),
    }
}

func (fus *FileUploadService) ValidateFile(fileHeader *multipart.FileHeader) error {
    // Check file size
    if fileHeader.Size > fus.maxFileSize {
        return errors.New("file size exceeds limit")
    }

    // Check file extension
    ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
    if !fus.isAllowedExtension(ext) {
        return errors.New("file type not allowed")
    }

    // Open file to check MIME type
    file, err := fileHeader.Open()
    if err != nil {
        return err
    }
    defer file.Close()

    // Read first 512 bytes to detect MIME type
    buffer := make([]byte, 512)
    _, err = file.Read(buffer)
    if err != nil {
        return err
    }

    mimeType := http.DetectContentType(buffer)
    if !fus.isAllowedMimeType(mimeType) {
        return errors.New("file content type not allowed")
    }

    return nil
}

func (fus *FileUploadService) SaveFile(fileHeader *multipart.FileHeader, userID string) (string, error) {
    if err := fus.ValidateFile(fileHeader); err != nil {
        return "", err
    }

    // Generate secure filename
    ext := filepath.Ext(fileHeader.Filename)
    filename := fmt.Sprintf("%s_%d%s",
        generateSecureID(),
        time.Now().Unix(),
        ext)

    // Create user-specific directory
    userDir := filepath.Join(fus.uploadDir, userID)
    if err := os.MkdirAll(userDir, 0755); err != nil {
        return "", err
    }

    filepath := filepath.Join(userDir, filename)

    // Save file
    src, err := fileHeader.Open()
    if err != nil {
        return "", err
    }
    defer src.Close()

    dst, err := os.Create(filepath)
    if err != nil {
        return "", err
    }
    defer dst.Close()

    _, err = io.Copy(dst, src)
    return filename, err
}

func generateSecureID() string {
    b := make([]byte, 16)
    rand.Read(b)
    return hex.EncodeToString(b)
}

Security Headers

Security Headers Middleware

func SecurityHeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Prevent MIME sniffing
        c.Header("X-Content-Type-Options", "nosniff")

        // Prevent clickjacking
        c.Header("X-Frame-Options", "DENY")

        // Enable XSS protection
        c.Header("X-XSS-Protection", "1; mode=block")

        // Enforce HTTPS
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")

        // Control referrer information
        c.Header("Referrer-Policy", "strict-origin-when-cross-origin")

        // Permissions policy
        c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")

        c.Next()
    }
}

Audit Logging

Security Event Logging

type SecurityLogger struct {
    logger *log.Logger
}

func NewSecurityLogger(logger *log.Logger) *SecurityLogger {
    return &SecurityLogger{logger: logger}
}

func (sl *SecurityLogger) LogSecurityEvent(event string, userID string, details map[string]interface{}) {
    logEntry := map[string]interface{}{
        "timestamp": time.Now().UTC(),
        "event":     event,
        "user_id":   userID,
        "details":   details,
    }

    jsonData, _ := json.Marshal(logEntry)
    sl.logger.Printf("SECURITY: %s", string(jsonData))
}

// Usage in authentication
func (as *AuthService) Login(email, password string, clientIP string) (*mdl.User, string, error) {
    user, exists, err := as.userFetcher.FindOneByEmail(context.Background(), email, nil)
    if err != nil || !exists {
        as.securityLogger.LogSecurityEvent("login_failed", "", map[string]interface{}{
            "email":     email,
            "client_ip": clientIP,
            "reason":    "user_not_found",
        })
        return nil, "", errors.New("invalid credentials")
    }

    if !as.passwordService.CheckPassword(password, user.PasswordHash) {
        as.securityLogger.LogSecurityEvent("login_failed", user.Id, map[string]interface{}{
            "client_ip": clientIP,
            "reason":    "invalid_password",
        })
        return nil, "", errors.New("invalid credentials")
    }

    token, err := as.GenerateToken(user)
    if err != nil {
        return nil, "", err
    }

    as.securityLogger.LogSecurityEvent("login_success", user.Id, map[string]interface{}{
        "client_ip": clientIP,
    })

    return user, token, nil
}

Security should be built into every layer of your application. Regular security audits and staying updated with the latest security practices are essential for maintaining a secure application.