Chapter 47

Authentication and Security

Authentication and Security

In 2013, Adobe suffered one of the most notorious data breaches in history: attackers stole account information for 153 million users. The most damaging aspect wasn't the breach itselfโ€”it was that Adobe had encrypted (not hashed) user passwords using 3DES. When the encryption keys were also compromised, all passwords were effectively exposed in plaintext. Months later, security researchers discovered something even more alarming: by cross-referencing the password hint field with the encrypted password field, passwords like "123456" were identified on the first day the data was published.

This case exposes the most fundamental truth about web security: developer intuitions about "secure" are often wrong. Encryption is not hashing. Authentication is not authorization. HTTPS does not mean your application is secure.

This chapter's goal isn't to hand you a security checklist to memorize. It's to build a mental model for thinking about security problemsโ€”understanding the attacker's perspective, the threat model behind each defensive mechanism, and how to implement them correctly in Go.

Level 1 ยท What You Need to Know

Authentication vs. Authorization: Two Commonly Conflated Concepts

Authentication (AuthN) answers "Who are you?"โ€”it verifies the identity of the user. The user provides credentials (password, token, certificate), and the system validates those credentials to confirm who is making the request.

Authorization (AuthZ) answers "What can you do?"โ€”given that identity has been confirmed, it determines whether that user has permission to perform a specific action.

The relationship is sequential: authenticate first, then authorize. But they are fundamentally different concerns in system design. Conflating them leads to serious vulnerabilities:

OWASP (Open Web Application Security Project) lists "Broken Access Control" and "Broken Authentication" as the top two web application security threats. In Go applications, both problems typically share the same root cause: developers assume certain paths "won't be directly accessed by users."

OWASP Top 10 in the Go Context

The OWASP Top 10 is the most important reference document in web security, updated every few years. Here are the most prevalent issues in Go web applications:

A01 - Broken Access Control

The classic example is horizontal privilege escalation: User A can access User B's private data because the server only checked "is the user logged in," not "does this user own this resource."

// Dangerous: only validates login state, not resource ownership
func getOrder(w http.ResponseWriter, r *http.Request) {
    orderID := r.URL.Query().Get("id")
    order := db.GetOrder(orderID) // No check: order.UserID == currentUser.ID
    json.NewEncoder(w).Encode(order)
}

A02 - Cryptographic Failures

Adobe's case falls here. Common mistakes in Go include: using MD5 or SHA1 to hash passwords (rainbow table attacks), using ECB mode for block ciphers (insecure), using a weak random number generator (math/rand) for keys or tokens.

A03 - Injection

SQL injection remains one of the most common high-severity vulnerabilities. In Go, string concatenation is the root cause:

// Extremely dangerous: directly concatenating user input
query := "SELECT * FROM users WHERE username = '" + username + "'"

A07 - Identification and Authentication Failures

Includes weak password policies, insecure "remember me" implementations, missing multi-factor authentication, and session fixation attacks.

A10 - Server-Side Request Forgery (SSRF)

When your Go service makes HTTP requests to URLs provided by users, attackers can craft URLs pointing to internal services (like http://169.254.169.254/, the AWS metadata endpoint) to enumerate the internal network or steal cloud credentials.

The Fundamentals of Password Security

Never store plaintext passwords or reversibly encrypted passwords. What systems should store is a hash of the password, and it must use algorithms specifically designed for password hashing.

Why can't you use a standard hash like SHA256? Because standard hash algorithms are designed to be as fast as possible. Attackers with GPUs can compute billions of SHA256 hashes per second, making brute-force attacks feasible.

bcrypt is the most widely used password hashing algorithm today. It's designed to be deliberately slowโ€”the cost parameter controls computational expense and can be increased as CPU performance improves. bcrypt also builds in a salt (random value), preventing rainbow table attacks and ensuring that two users with the same password store different hash values.

golang.org/x/crypto/bcrypt is Go's official extended package providing a security-compliant bcrypt implementation.


Level 2 ยท Principles and Mechanisms

JWT: Structure and Pitfalls

JWT (JSON Web Token) is the dominant approach for stateless authentication. A JWT consists of three parts joined by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header (first segment, Base64URL decoded):

{"alg": "HS256", "typ": "JWT"}

Payload (second segment, Base64URL decoded):

{"sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622}

Signature (third segment):

HMACSHA256(base64url(header) + "." + base64url(payload), secret)

Critical understanding: JWT Payload is Base64 encoded, not encrypted. Anyone can decode and read the Payload. The signature verifies that the Payload hasn't been tampered withโ€”it does not hide the content. Therefore, never put sensitive information (passwords, credit card numbers) in a JWT Payload.

Common JWT Security Pitfalls:

Pitfall 1: Algorithm Confusion Attack

Early JWT libraries allowed servers to read the algorithm type from the Header's alg field. Attackers could change alg from RS256 (asymmetric signing) to HS256 (symmetric signing), then use the server's public key (publicly obtainable) as the HS256 secret to forge arbitrary JWTs.

Defense: Explicitly specify the expected algorithm during validation. Never trust the alg field in the Header.

Pitfall 2: Missing exp Validation

Some implementations verify the signature but forget to check exp (expiration time), allowing expired tokens to remain valid indefinitely. When using golang-jwt/jwt, expiration is validated by defaultโ€”but ensure you never pass WithoutClaimsValidation().

Pitfall 3: Empty Secret Key

If the secret is an empty string, some libraries still successfully validate the signature (because HMAC accepts empty keys). Assert at application startup that the secret is non-empty and sufficiently long (at least 256 bits).

OAuth2 Authorization Code Flow with PKCE

OAuth2 is a protocol framework for "letting third-party applications access resources on behalf of users." It is not an authentication protocol (though OpenID Connect builds an authentication layer on top of it).

Authorization Code Flow is the most secure OAuth2 flow, suitable for web applications with a backend:

1. User clicks "Sign in with Google"
2. App generates random state, redirects to Google's authorization page
   GET https://accounts.google.com/o/oauth2/auth?
     client_id=YOUR_CLIENT_ID&
     redirect_uri=https://yourapp.com/callback&
     response_type=code&
     scope=openid email profile&
     state=RANDOM_STRING

3. User consents on Google
4. Google redirects back to app callback with code and state
   GET https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STRING

5. App validates state, exchanges code for access_token and refresh_token
   POST https://oauth2.googleapis.com/token
   {code, client_id, client_secret, redirect_uri, grant_type=authorization_code}

6. Google returns access_token (and id_token for OpenID Connect)
7. App uses access_token to fetch user info

PKCE (Proof Key for Code Exchange) enhances the authorization code flow, primarily to prevent authorization code interception attacks. It's especially important for public clients (mobile apps, SPAs):

1. App generates random code_verifier (43-128 bytes random string)
2. Computes code_challenge = BASE64URL(SHA256(code_verifier))
3. Authorization request includes code_challenge and code_challenge_method=S256
4. Token exchange includes code_verifier
5. Authorization server verifies SHA256(code_verifier) == code_challenge

Even if an authorization code is intercepted by a man-in-the-middle, the attacker cannot exchange it for a token without the code_verifier.

Refresh Token Rotation

Access Tokens are designed to be short-lived (typically 15 minutes to 1 hour). After expiration, a Refresh Token is used to obtain a new Access Token.

Refresh Token Rotation is the key practice for improved security: each time a Refresh Token is used, the server issues a new Refresh Token and immediately invalidates the old one. If an attacker steals a Refresh Token and tries to use it, the server detects that "an old token is being reused," identifies a potential token leak, and revokes the entire token family.

Cookie-based sessions: The server generates a random session ID and stores it in a database or Redis. The client carries the session ID via Cookie. Pros: can be immediately revoked (delete the database record). Cons: requires server-side storage; horizontal scaling requires shared session storage.

Token-based authentication (JWT): The server is stateless, requiring no session storage. Pros: easily horizontally scalable. Cons: cannot be immediately revoked (a token remains valid until exp unless you maintain a token blacklistโ€”which reintroduces stateful storage).

Recommendation: For small-scale applications with manageable session volumes, cookie-based server-side sessions are often more secure and simpler. JWT has clear advantages for service-to-service verification in microservices (short-lived tokens).


Level 3 ยท Code Practice

bcrypt Password Hashing

package auth

import (
    "errors"
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

const bcryptCost = 12 // Recommended: 10-14, adjust based on server performance

// HashPassword generates a bcrypt hash of the password
func HashPassword(password string) (string, error) {
    if len(password) == 0 {
        return "", errors.New("password cannot be empty")
    }
    // bcrypt automatically generates a random salt and includes it in the output
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
    if err != nil {
        return "", fmt.Errorf("hash password: %w", err)
    }
    return string(bytes), nil
}

// CheckPassword verifies whether a password matches its hash.
// Note: this function runs in constant time to prevent timing attacks.
func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

JWT Middleware: Complete Implementation

package middleware

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "strings"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

// Loaded from environment variablesโ€”never hard-coded
var jwtSecret = []byte(mustGetEnv("JWT_SECRET"))

type Claims struct {
    UserID   int64  `json:"uid"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

type contextKey string

const claimsKey contextKey = "claims"

// GenerateAccessToken creates a short-lived Access Token (15 minutes)
func GenerateAccessToken(userID int64, username, role string) (string, error) {
    claims := Claims{
        UserID:   userID,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
            Subject:   fmt.Sprintf("%d", userID),
        },
    }

    // Explicitly specify algorithm to prevent algorithm confusion attacks
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

// GenerateRefreshToken creates a long-lived Refresh Token (7 days), stored to database
func GenerateRefreshToken(userID int64) (string, string, error) {
    // Generate random token ID for revocation tracking
    tokenID, err := generateSecureRandom(32)
    if err != nil {
        return "", "", err
    }

    claims := jwt.RegisteredClaims{
        ID:        tokenID,
        Subject:   fmt.Sprintf("%d", userID),
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
        Issuer:    "myapp",
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    signed, err := token.SignedString(jwtSecret)
    return signed, tokenID, err
}

// JWTMiddleware validates the Authorization: Bearer <token> header
func JWTMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "missing authorization header", http.StatusUnauthorized)
            return
        }

        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
            http.Error(w, "invalid authorization header format", http.StatusUnauthorized)
            return
        }

        tokenStr := parts[1]

        // Critical: explicitly specify the expected signing algorithm.
        // Prevents algorithm confusion: if token uses RS256, validation fails.
        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
            // Verify algorithm type
            if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
            }
            return jwtSecret, nil
        })

        if err != nil {
            // Distinguish between expired and invalid tokens
            if errors.Is(err, jwt.ErrTokenExpired) {
                http.Error(w, "token expired", http.StatusUnauthorized)
            } else {
                http.Error(w, "invalid token", http.StatusUnauthorized)
            }
            return
        }

        if !token.Valid {
            http.Error(w, "invalid token", http.StatusUnauthorized)
            return
        }

        // Store claims in context for downstream handlers
        ctx := context.WithValue(r.Context(), claimsKey, claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// GetClaims extracts claims from context
func GetClaims(ctx context.Context) (*Claims, bool) {
    claims, ok := ctx.Value(claimsKey).(*Claims)
    return claims, ok
}

Refresh Token Rotation Implementation

package handler

import (
    "database/sql"
    "encoding/json"
    "net/http"
    "time"
)

type RefreshRequest struct {
    RefreshToken string `json:"refresh_token"`
}

type TokenResponse struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    ExpiresIn    int    `json:"expires_in"` // seconds
}

func (h *Handler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
    var req RefreshRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "invalid request", http.StatusBadRequest)
        return
    }

    // Validate the Refresh Token
    claims, err := validateRefreshToken(req.RefreshToken)
    if err != nil {
        http.Error(w, "invalid refresh token", http.StatusUnauthorized)
        return
    }

    // Look up the token in the database to check if it's been used or revoked
    storedToken, err := h.db.GetRefreshToken(claims.ID)
    if err == sql.ErrNoRows {
        // Token not found: either revoked or a token replay attack occurred.
        // With rotation: an old token being reused signals potential compromise.
        // Security measure: revoke ALL tokens for this user.
        userID := parseUserID(claims.Subject)
        h.db.RevokeAllTokensForUser(userID)
        http.Error(w, "refresh token revoked", http.StatusUnauthorized)
        return
    }

    if storedToken.RevokedAt != nil {
        http.Error(w, "refresh token revoked", http.StatusUnauthorized)
        return
    }

    if storedToken.ExpiresAt.Before(time.Now()) {
        http.Error(w, "refresh token expired", http.StatusUnauthorized)
        return
    }

    // Revoke the old Refresh Token (rotation)
    h.db.RevokeRefreshToken(claims.ID)

    // Fetch user information
    user, err := h.db.GetUser(storedToken.UserID)
    if err != nil {
        http.Error(w, "user not found", http.StatusUnauthorized)
        return
    }

    // Generate new Access Token and Refresh Token
    accessToken, err := GenerateAccessToken(user.ID, user.Username, user.Role)
    if err != nil {
        http.Error(w, "failed to generate token", http.StatusInternalServerError)
        return
    }

    newRefreshToken, tokenID, err := GenerateRefreshToken(user.ID)
    if err != nil {
        http.Error(w, "failed to generate refresh token", http.StatusInternalServerError)
        return
    }

    // Store the new Refresh Token in the database
    h.db.StoreRefreshToken(tokenID, user.ID, time.Now().Add(7*24*time.Hour))

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: newRefreshToken,
        ExpiresIn:    900, // 15 minutes
    })
}

OAuth2 Google Login

package oauth

import (
    "context"
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "net/http"

    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

var googleOAuthConfig = &oauth2.Config{
    ClientID:     mustGetEnv("GOOGLE_CLIENT_ID"),
    ClientSecret: mustGetEnv("GOOGLE_CLIENT_SECRET"),
    RedirectURL:  mustGetEnv("GOOGLE_REDIRECT_URL"),
    Scopes:       []string{"openid", "email", "profile"},
    Endpoint:     google.Endpoint,
}

// GoogleLoginHandler redirects to Google's OAuth page
func GoogleLoginHandler(w http.ResponseWriter, r *http.Request) {
    // Generate random state to prevent CSRF
    state, err := generateState()
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }

    // Store state in a session or short-lived cookie
    http.SetCookie(w, &http.Cookie{
        Name:     "oauth_state",
        Value:    state,
        MaxAge:   300, // 5 minutes
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
    })

    url := googleOAuthConfig.AuthCodeURL(state,
        oauth2.AccessTypeOffline, // request refresh_token
    )
    http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

// GoogleCallbackHandler handles the OAuth callback
func GoogleCallbackHandler(w http.ResponseWriter, r *http.Request) {
    // Validate state to prevent CSRF
    stateCookie, err := r.Cookie("oauth_state")
    if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
        http.Error(w, "invalid state", http.StatusBadRequest)
        return
    }

    // Clear state cookie
    http.SetCookie(w, &http.Cookie{Name: "oauth_state", MaxAge: -1})

    code := r.URL.Query().Get("code")
    if code == "" {
        http.Error(w, "missing code", http.StatusBadRequest)
        return
    }

    // Exchange code for token
    token, err := googleOAuthConfig.Exchange(r.Context(), code)
    if err != nil {
        http.Error(w, "failed to exchange token", http.StatusInternalServerError)
        return
    }

    // Fetch user info
    client := googleOAuthConfig.Client(r.Context(), token)
    resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
    if err != nil {
        http.Error(w, "failed to get user info", http.StatusInternalServerError)
        return
    }
    defer resp.Body.Close()

    var userInfo GoogleUserInfo
    if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
        http.Error(w, "failed to decode user info", http.StatusInternalServerError)
        return
    }

    // Find or create user
    user, err := findOrCreateGoogleUser(userInfo)
    if err != nil {
        http.Error(w, "failed to process user", http.StatusInternalServerError)
        return
    }

    // Generate JWT
    accessToken, _ := GenerateAccessToken(user.ID, user.Username, user.Role)
    refreshToken, tokenID, _ := GenerateRefreshToken(user.ID)
    storeRefreshToken(tokenID, user.ID)

    http.Redirect(w, r, "/dashboard?token="+accessToken+"&refresh="+refreshToken, http.StatusFound)
}

func generateState() (string, error) {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(b), nil
}

Login Rate Limiting

package ratelimit

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

// LoginRateLimiter implements per-IP + per-username login rate limiting.
// For production with multiple instances, use a Redis-backed implementation.
type LoginRateLimiter struct {
    mu       sync.Mutex
    attempts map[string]*attemptRecord
}

type attemptRecord struct {
    count       int
    lastReset   time.Time
    lockedUntil time.Time
}

const (
    maxAttempts    = 5
    windowDuration = 15 * time.Minute
    lockDuration   = 30 * time.Minute
)

func NewLoginRateLimiter() *LoginRateLimiter {
    rl := &LoginRateLimiter{
        attempts: make(map[string]*attemptRecord),
    }
    go rl.cleanup()
    return rl
}

func (rl *LoginRateLimiter) key(r *http.Request, username string) string {
    ip := r.RemoteAddr // In production, handle X-Forwarded-For
    return fmt.Sprintf("%s:%s", ip, username)
}

// Allow checks whether a login attempt should be permitted
func (rl *LoginRateLimiter) Allow(r *http.Request, username string) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    key := rl.key(r, username)
    record, exists := rl.attempts[key]
    if !exists {
        rl.attempts[key] = &attemptRecord{count: 0, lastReset: time.Now()}
        return true
    }

    if time.Now().Before(record.lockedUntil) {
        return false
    }

    if time.Since(record.lastReset) > windowDuration {
        record.count = 0
        record.lastReset = time.Now()
    }

    return record.count < maxAttempts
}

// RecordFailure records a failed login attempt
func (rl *LoginRateLimiter) RecordFailure(r *http.Request, username string) {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    key := rl.key(r, username)
    record, exists := rl.attempts[key]
    if !exists {
        rl.attempts[key] = &attemptRecord{count: 1, lastReset: time.Now()}
        return
    }

    record.count++
    if record.count >= maxAttempts {
        record.lockedUntil = time.Now().Add(lockDuration)
    }
}

// RecordSuccess resets the counter after a successful login
func (rl *LoginRateLimiter) RecordSuccess(r *http.Request, username string) {
    rl.mu.Lock()
    defer rl.mu.Unlock()
    delete(rl.attempts, rl.key(r, username))
}

Security HTTP Headers

package middleware

import "net/http"

// SecurityHeaders adds critical security HTTP response headers
func SecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // HSTS: force browsers to use HTTPS only; includeSubDomains covers subdomains
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")

        // CSP: restrict resource loading origins to prevent XSS
        w.Header().Set("Content-Security-Policy",
            "default-src 'self'; "+
                "script-src 'self' 'nonce-{{NONCE}}'; "+ // per-request random nonce
                "style-src 'self' 'unsafe-inline'; "+
                "img-src 'self' data: https:; "+
                "frame-ancestors 'none'")

        // Prevent clickjacking via iframe embedding
        w.Header().Set("X-Frame-Options", "DENY")

        // Disable MIME type sniffing
        w.Header().Set("X-Content-Type-Options", "nosniff")

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

        // Permissions policy: disable unneeded browser features
        w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")

        next.ServeHTTP(w, r)
    })
}

Level 4 ยท Advanced and Edge Cases

Paseto: A Safer JWT Alternative

JWT has a fundamental design flaw: it allows algorithm negotiation (via the Header's alg field), which is the root of algorithm confusion attacks. Paseto (Platform-Agnostic Security Tokens) solves this by eliminating algorithm flexibilityโ€”each Paseto version and purpose maps to a fixed algorithm with no choices available.

Paseto has four versions (v1-v4); v4 is recommended:

import "github.com/o1ecc8b/paseto"

// Using v4.local (symmetric encryption)
key, _ := paseto.NewV4SymmetricKey()
token, _ := paseto.NewEncryptedToken().
    SetSubject("user-123").
    SetExpiration(time.Now().Add(15 * time.Minute)).
    SetClaim("role", "admin").
    Encrypt(key, nil)

// Validate and decrypt
parsedToken, err := paseto.NewParser().ParseV4Local(key, token, nil)

Compared to JWT, Paseto's local tokens also encrypt the payloadโ€”even if a token leaks, attackers cannot read its contents.

FIDO2/WebAuthn: Passwordless Authentication

WebAuthn is a W3C standard that allows users to authenticate using biometrics (fingerprint, face) or hardware keys (YubiKey) instead of passwords. Its core mechanism is public-key cryptography:

  1. Registration: The user's device generates a key pair. The private key stays on the device (never leaves it); the public key is sent to the server.
  2. Authentication: The server sends a random challenge; the device signs it with the private key; the server verifies with the public key.

In Go, use github.com/go-webauthn/webauthn:

import "github.com/go-webauthn/webauthn/webauthn"

wconfig := &webauthn.Config{
    RPDisplayName: "My App",
    RPID:          "example.com",
    RPOrigins:     []string{"https://example.com"},
}
web, _ := webauthn.New(wconfig)

// Registration: generate options for the frontend
options, sessionData, _ := web.BeginRegistration(user)

// Registration: process credential returned from frontend
credential, _ := web.FinishRegistration(user, sessionData, r)
user.AddCredential(credential)

mTLS: Service-to-Service Authentication

In microservice architectures, how does Service A prove "I am the legitimate Service A, not an attacker"? mTLS (Mutual TLS) requires bidirectional certificate verificationโ€”not only does the server present a certificate to the client, but the client must also present one to the server.

// Server-side mTLS configuration
tlsConfig := &tls.Config{
    ClientAuth: tls.RequireAndVerifyClientCert,
    ClientCAs:  certPool, // pool of trusted CA certificates
    MinVersion: tls.VersionTLS13,
}
server := &http.Server{
    TLSConfig: tlsConfig,
}

// Client-side: present a certificate
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{cert},
    RootCAs:      serverCertPool,
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}

In production, certificate rotation is the key challenge. SPIFFE/SPIRE is an identity framework designed specifically for mTLS, capable of automatically distributing and rotating short-lived certificates (valid for hours).

Secret Management: Vault Integration

Hard-coding secrets is the most dangerous security anti-pattern. HashiCorp Vault provides centralized secret management:

import vault "github.com/hashicorp/vault/api"

func getSecretFromVault(secretPath string) (string, error) {
    config := vault.DefaultConfig()
    config.Address = os.Getenv("VAULT_ADDR")

    client, err := vault.NewClient(config)
    if err != nil {
        return "", err
    }

    // Use AppRole or Kubernetes auth to obtain a Vault token.
    // Avoid the Root token.
    client.SetToken(os.Getenv("VAULT_TOKEN"))

    secret, err := client.Logical().Read(secretPath)
    if err != nil {
        return "", err
    }

    value, ok := secret.Data["value"].(string)
    if !ok {
        return "", errors.New("secret not found")
    }
    return value, nil
}

Dynamic Secrets are Vault's killer feature: database passwords are no longer static. Vault creates a temporary user for each request and automatically deletes it when done. Even if credentials leak, the exposure window is extremely short.

SQL Injection Defense: Prepared Statements

In Go, using placeholder parameters with database/sql is the standard defense against SQL injection:

// Wrong: string concatenation
rows, _ := db.Query("SELECT * FROM users WHERE name = '" + name + "'")

// Correct: use parameter placeholders
// MySQL uses ?, PostgreSQL uses $1, $2
rows, err := db.QueryContext(ctx,
    "SELECT id, email FROM users WHERE name = ? AND active = ?",
    name, true,
)

// Using sqlx named parameters (more readable)
rows, err := db.NamedQueryContext(ctx,
    "SELECT * FROM users WHERE name = :name AND role = :role",
    map[string]interface{}{"name": name, "role": role},
)

database/sql placeholder parameters are handled at the driver level with parameterized queries. The database server parses SQL structure and data separately, making injection structurally impossible.

Go Templates and XSS Defense

The html/template package (not text/template) performs context-aware escaping on all data inserted into HTML:

import "html/template"

// html/template auto-escapes to prevent XSS
tmpl := template.Must(template.ParseFiles("user.html"))
tmpl.Execute(w, map[string]interface{}{
    "Username": userInput, // "<script>alert('xss')</script>" gets escaped
})

// Only use template.HTML when you are certain the content is safe.
// Example: Markdown โ†’ HTML content you generated yourself.
safeHTML := template.HTML(markdownToHTML(content))

html/template understands HTML structure and uses different escaping rules in HTML attributes, JavaScript contexts, and CSS contextsโ€”more secure than naive &lt; replacement.

Cryptographically Secure Random Numbers

Go's standard library has two random number packages that must be clearly distinguished:

import (
    "crypto/rand"  // Cryptographically secure; use for tokens and key generation
    "math/rand"    // Pseudorandom; use for games and tests; NEVER for security
)

// Generate a secure random token
func generateSecureToken(n int) (string, error) {
    bytes := make([]byte, n)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(bytes), nil
}

math/rand output is predictableโ€”knowing enough output values, an attacker can reconstruct the internal state and predict future outputs. crypto/rand uses the operating system's cryptographically secure random source (on Linux: /dev/urandom or the getrandom() syscall).


Security is not a feature; it is a system property. Every technique covered in this chapter has a corresponding threat model. Remember this principle: do not attempt to invent your own cryptographyโ€”Go's standard library and audited third-party libraries already implement security algorithms proven over decades. Your job is to combine and use them correctly, understand their boundary conditions, and stay current with security advisories.

Rate this chapter
4.8  / 5  (3 ratings)

๐Ÿ’ฌ Comments