Security Best Practices in Go
Security is a critical aspect of any application. This guide covers best practices for implementing security in Go applications.
Authentication
1. Password Handling
Secure password storage and validation:
import (
"golang.org/x/crypto/bcrypt"
)
// Hash password before storage
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(bytes), nil
}
// Verify password
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// Password validation
func ValidatePassword(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters")
}
var (
hasUpper bool
hasLower bool
hasNumber bool
hasSpecial bool
)
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
return errors.New("password must contain upper, lower, number, and special characters")
}
return nil
}
2. JWT Authentication
Implement JWT-based authentication:
import (
"github.com/golang-jwt/jwt/v4"
)
type Claims struct {
UserID string `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateToken(userID, role string, secret []byte) (string, error) {
claims := Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
func ValidateToken(tokenString string, secret []byte) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, 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 secret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
Authorization
1. Role-Based Access Control (RBAC)
Implement RBAC:
type Permission string
const (
PermissionRead Permission = "read"
PermissionWrite Permission = "write"
PermissionDelete Permission = "delete"
)
type Role struct {
Name string
Permissions []Permission
}
type RBAC struct {
roles map[string]Role
}
func NewRBAC() *RBAC {
return &RBAC{
roles: make(map[string]Role),
}
}
func (r *RBAC) AddRole(role Role) {
r.roles[role.Name] = role
}
func (r *RBAC) HasPermission(role string, permission Permission) bool {
if r, ok := r.roles[role]; ok {
for _, p := range r.Permissions {
if p == permission {
return true
}
}
}
return false
}
// Middleware for RBAC
func RBACMiddleware(rbac *RBAC, requiredPermission Permission) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value("claims").(*Claims)
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !rbac.HasPermission(claims.Role, requiredPermission) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
2. Context-Based Authorization
Implement context-based authorization:
type AuthorizationContext struct {
UserID string
Role string
Scopes []string
}
type Resource struct {
ID string
OwnerID string
Type string
}
type AuthorizationService struct {
rbac *RBAC
}
func (s *AuthorizationService) CanAccess(ctx AuthorizationContext, resource Resource, action string) bool {
// Check ownership
if resource.OwnerID == ctx.UserID {
return true
}
// Check role-based permissions
if s.rbac.HasPermission(ctx.Role, Permission(action)) {
return true
}
// Check scopes
for _, scope := range ctx.Scopes {
if scope == fmt.Sprintf("%s:%s", resource.Type, action) {
return true
}
}
return false
}
Data Protection
1. Encryption
Implement data encryption:
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
type Encryption struct {
key []byte
}
func NewEncryption(key []byte) *Encryption {
return &Encryption{key: key}
}
func (e *Encryption) Encrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(e.key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, data, nil), nil
}
func (e *Encryption) Decrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(e.key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
2. Input Validation
Implement input validation:
import (
"github.com/go-playground/validator/v10"
)
type UserInput struct {
Username string `validate:"required,min=3,max=50,alphanum"`
Email string `validate:"required,email"`
Age int `validate:"required,gte=18,lte=120"`
}
func ValidateInput(input interface{}) error {
validate := validator.New()
return validate.Struct(input)
}
// Custom validation
func ValidateSQL(fl validator.FieldLevel) bool {
value := fl.Field().String()
// Check for SQL injection patterns
dangerousPatterns := []string{"DROP", "DELETE", "UPDATE", "INSERT", "--", "/*", "*/"}
for _, pattern := range dangerousPatterns {
if strings.Contains(strings.ToUpper(value), pattern) {
return false
}
}
return true
}
HTTP Security
1. Secure Headers
Implement secure HTTP headers:
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Security headers
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
2. CSRF Protection
Implement CSRF protection:
import (
"github.com/gorilla/csrf"
)
func main() {
CSRF := csrf.Protect(
[]byte("32-byte-long-auth-key"),
csrf.Secure(true),
csrf.HttpOnly(true),
)
router := mux.NewRouter()
router.Use(CSRF)
http.ListenAndServe(":8000", router)
}
func formHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-CSRF-Token", csrf.Token(r))
// Handle form
}
Logging and Monitoring
1. Security Logging
Implement security event logging:
type SecurityEvent struct {
Timestamp time.Time
EventType string
UserID string
IP string
Action string
Status string
Description string
}
func LogSecurityEvent(event SecurityEvent) {
log.Info().
Time("timestamp", event.Timestamp).
Str("event_type", event.EventType).
Str("user_id", event.UserID).
Str("ip", event.IP).
Str("action", event.Action).
Str("status", event.Status).
Str("description", event.Description).
Msg("security event")
}
// Usage
func LoginHandler(w http.ResponseWriter, r *http.Request) {
success := authenticate(r)
LogSecurityEvent(SecurityEvent{
Timestamp: time.Now(),
EventType: "authentication",
UserID: r.Form.Get("username"),
IP: r.RemoteAddr,
Action: "login",
Status: fmt.Sprintf("%v", success),
Description: "User login attempt",
})
}
2. Rate Limiting
Implement rate limiting:
import (
"golang.org/x/time/rate"
)
type IPRateLimiter struct {
ips map[string]*rate.Limiter
mu *sync.RWMutex
r rate.Limit
b int
}
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
return &IPRateLimiter{
ips: make(map[string]*rate.Limiter),
mu: &sync.RWMutex{},
r: r,
b: b,
}
}
func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter {
i.mu.Lock()
defer i.mu.Unlock()
limiter := rate.NewLimiter(i.r, i.b)
i.ips[ip] = limiter
return limiter
}
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
i.mu.Lock()
limiter, exists := i.ips[ip]
if !exists {
i.mu.Unlock()
return i.AddIP(ip)
}
i.mu.Unlock()
return limiter
}
func RateLimitMiddleware(limiter *IPRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.GetLimiter(r.RemoteAddr).Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
Next Steps
- Learn about Error Handling
- Explore Logging
- Study Performance