Authentication and Authorization in Go Gin

Learning Objectives: By the end of this lesson, you will be able to:

  • Implement JWT-based authentication with refresh tokens
  • Build session-based authentication systems
  • Integrate OAuth2 providers (Google, GitHub, Facebook)
  • Implement role-based access control (RBAC) systems
  • Secure password handling with proper hashing and validation
  • Create multi-factor authentication (MFA) flows
  • Implement API key authentication for external services
  • Apply security best practices and handle common vulnerabilities

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever wondered how applications like GitHub know who you are when you visit different pages, or how they determine whether you can delete a repository? Or perhaps you’ve been frustrated by complex login flows and wondered how to build authentication that’s both secure and user-friendly? Today, we’re diving deep into authentication and authorization - the critical systems that protect user data and control access to resources.

Lesson Overview

  • Duration: 140 minutes
  • Prerequisites: Lessons 1-6 completed, understanding of HTTP headers, cookies, and basic cryptography
  • Materials Needed: Go 1.25+ for Gin v1.12, Gin framework, OAuth provider accounts for testing

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Authentication Fundamentals - Understanding different authentication methods and when to use them
  2. JWT Implementation - Building secure token-based authentication with refresh strategies
  3. Session Management - Implementing server-side session storage and validation
  4. OAuth Integration - Adding third-party authentication providers
  5. Authorization Systems - Building flexible role and permission-based access control
  6. Advanced Security - MFA, API keys, and security best practices
  7. Real-world Implementation - Building a complete authentication system

Success Criteria:

By the end of this session, you should be able to build production-ready authentication systems that are secure, scalable, and provide excellent user experience.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Authentication vs. Authorization

  • Authentication: Verifying “who you are” (login credentials, tokens, biometrics)
  • Authorization: Determining “what you can do” (permissions, roles, access levels)

Real-World Analogy: Think of a hotel - authentication is showing your ID at check-in to prove you’re the person who booked the room, while authorization is your room key that only opens certain doors (your room, gym, but not other guests’ rooms).

Why This Matters: Many security breaches occur because developers confuse these concepts or implement one without the other. Both are essential for a secure system.

Authentication Methods Comparison

Method Pros Cons Best For
JWT Stateless, scalable, cross-domain Token size, logout complexity APIs, microservices
Sessions Simple logout, server control Server state, scaling challenges Traditional web apps
OAuth Third-party trust, user convenience Dependency, complex flows Social logins
API Keys Simple for services Not suitable for users Service-to-service

Role-Based Access Control (RBAC)

RBAC provides a flexible way to manage permissions:

  • Users have Roles
  • Roles have Permissions
  • Permissions control access to Resources

Security Principles

  1. Principle of Least Privilege: Users get minimum permissions needed
  2. Defense in Depth: Multiple security layers
  3. Fail Secure: Systems fail to a secure state
  4. Zero Trust: Verify everything, trust nothing

Key Terminology

  • JWT (JSON Web Token): Self-contained token containing user information
  • Refresh Token: Long-lived token used to obtain new access tokens
  • Salt: Random data added to passwords before hashing
  • CSRF (Cross-Site Request Forgery): Attack where malicious sites send requests as authenticated users
  • Rate Limiting: Controlling the number of requests to prevent abuse
  • MFA (Multi-Factor Authentication): Using multiple authentication factors

4. Detailed Explanation and Examples (25-30 minutes)

Understanding JWT Architecture

JWTs consist of three parts separated by dots:

  1. Header: Specifies algorithm and token type
  2. Payload: Contains claims (user data, expiration, etc.)
  3. Signature: Ensures token integrity

JWT vs Session Trade-offs

JWT Benefits:

  • Stateless (no server storage required)
  • Cross-domain compatible
  • Contains user information
  • Scales horizontally

JWT Challenges:

  • Larger than session IDs
  • Cannot be revoked easily
  • Token theft concerns
  • Storage considerations

OAuth2 Flow Types

  1. Authorization Code Flow: Most secure, for web applications
  2. Implicit Flow: Deprecated, was used for SPAs
  3. Client Credentials Flow: For service-to-service communication
  4. Resource Owner Password Flow: Direct username/password exchange

RBAC Implementation Strategies

  1. Simple Roles: Basic user/admin distinction
  2. Hierarchical Roles: Roles inherit from other roles
  3. Permission-based: Fine-grained permission assignment
  4. Attribute-based: Dynamic permissions based on context

Common Misconceptions

  1. Misconception: “JWTs are always more secure than sessions” Reality: Both have security trade-offs; choice depends on requirements and implementation.

  2. Misconception: “Password hashing with MD5/SHA1 is sufficient” Reality: These are fast hashes vulnerable to rainbow table attacks; use bcrypt, scrypt, or Argon2.

  3. Misconception: “OAuth means you don’t need to handle passwords” Reality: You still need local authentication for users who don’t use third-party providers.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Complete JWT Authentication System

// auth/jwt.go
package auth

import (
    "crypto/rand"
    "encoding/hex"
    "errors"
    "time"
    
    "github.com/golang-jwt/jwt/v5"
    "golang.org/x/crypto/bcrypt"
)

// JWTConfig holds JWT configuration
type JWTConfig struct {
    SecretKey            []byte
    AccessTokenDuration  time.Duration
    RefreshTokenDuration time.Duration
    Issuer              string
}

// Claims represents JWT claims
type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    TokenType string `json:"token_type"` // "access" or "refresh"
    jwt.RegisteredClaims
}

// TokenPair represents access and refresh tokens
type TokenPair struct {
    AccessToken  string    `json:"access_token"`
    RefreshToken string    `json:"refresh_token"`
    ExpiresAt    time.Time `json:"expires_at"`
    TokenType    string    `json:"token_type"`
}

// JWTManager handles JWT operations
type JWTManager struct {
    config JWTConfig
}

// NewJWTManager creates a new JWT manager
func NewJWTManager(config JWTConfig) *JWTManager {
    return &JWTManager{config: config}
}

// GenerateTokenPair creates access and refresh tokens
func (j *JWTManager) GenerateTokenPair(userID uint, username, role string) (*TokenPair, error) {
    now := time.Now()
    
    // Create access token
    accessClaims := Claims{
        UserID:    userID,
        Username:  username,
        Role:      role,
        TokenType: "access",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(now.Add(j.config.AccessTokenDuration)),
            IssuedAt:  jwt.NewNumericDate(now),
            NotBefore: jwt.NewNumericDate(now),
            Issuer:    j.config.Issuer,
            Subject:   username,
        },
    }
    
    accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
    accessTokenString, err := accessToken.SignedString(j.config.SecretKey)
    if err != nil {
        return nil, err
    }
    
    // Create refresh token
    refreshClaims := Claims{
        UserID:    userID,
        Username:  username,
        Role:      role,
        TokenType: "refresh",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(now.Add(j.config.RefreshTokenDuration)),
            IssuedAt:  jwt.NewNumericDate(now),
            NotBefore: jwt.NewNumericDate(now),
            Issuer:    j.config.Issuer,
            Subject:   username,
        },
    }
    
    refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
    refreshTokenString, err := refreshToken.SignedString(j.config.SecretKey)
    if err != nil {
        return nil, err
    }
    
    return &TokenPair{
        AccessToken:  accessTokenString,
        RefreshToken: refreshTokenString,
        ExpiresAt:    accessClaims.ExpiresAt.Time,
        TokenType:    "Bearer",
    }, nil
}

// ValidateToken validates and parses a JWT token
func (j *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return j.config.SecretKey, nil
    }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithIssuer(j.config.Issuer))
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, errors.New("invalid token")
}

// RefreshAccessToken creates a new access token using refresh token
func (j *JWTManager) RefreshAccessToken(refreshTokenString string) (*TokenPair, error) {
    claims, err := j.ValidateToken(refreshTokenString)
    if err != nil {
        return nil, err
    }
    
    if claims.TokenType != "refresh" {
        return nil, errors.New("invalid token type")
    }
    
    // Generate new token pair. In production, rotate and revoke refresh tokens in
    // persistent storage and load the current role/status from the database.
    return j.GenerateTokenPair(claims.UserID, claims.Username, claims.Role)
}

// PasswordHasher handles password operations
type PasswordHasher struct {
    cost int
}

// NewPasswordHasher creates a new password hasher
func NewPasswordHasher() *PasswordHasher {
    return &PasswordHasher{cost: bcrypt.DefaultCost}
}

// HashPassword hashes a password
func (p *PasswordHasher) HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), p.cost)
    return string(bytes), err
}

// CheckPassword validates a password against hash
func (p *PasswordHasher) CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

// GenerateSalt creates a random salt
func GenerateSalt() (string, error) {
    bytes := make([]byte, 32)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

// handlers/auth_handler.go
package handlers

import (
    "net/http"
    "strings"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/your-app/auth"
    "github.com/your-app/models"
    "github.com/your-app/repositories"
)

// AuthHandler handles authentication requests
type AuthHandler struct {
    userRepo    repositories.UserRepository
    jwtManager  *auth.JWTManager
    passwordHasher *auth.PasswordHasher
}

// NewAuthHandler creates a new auth handler
func NewAuthHandler(userRepo repositories.UserRepository, jwtManager *auth.JWTManager) *AuthHandler {
    return &AuthHandler{
        userRepo:       userRepo,
        jwtManager:     jwtManager,
        passwordHasher: auth.NewPasswordHasher(),
    }
}

// LoginRequest represents login request data
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
    Remember bool   `json:"remember"`
}

// RegisterRequest represents registration request data
type RegisterRequest struct {
    Username        string `json:"username" binding:"required,min=3,max=30,alphanum"`
    Email          string  `json:"email" binding:"required,email"`
    Password       string  `json:"password" binding:"required,min=8"`
    ConfirmPassword string  `json:"confirm_password" binding:"required"`
    FullName       string  `json:"full_name" binding:"required,min=2,max=100"`
}

// Login authenticates user and returns JWT tokens
func (h *AuthHandler) Login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error":   "Invalid input",
            "details": err.Error(),
        })
        return
    }
    
    // Find user by username or email
    var user *models.User
    var err error
    
    if strings.Contains(req.Username, "@") {
        user, err = h.userRepo.GetByEmail(req.Username)
    } else {
        user, err = h.userRepo.GetByUsername(req.Username)
    }
    
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Invalid credentials",
        })
        return
    }
    
    // Check password
    if !h.passwordHasher.CheckPassword(req.Password, user.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Invalid credentials",
        })
        return
    }
    
    // Check if user is active
    if !user.Active {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Account is disabled",
        })
        return
    }
    
    // Generate tokens
    tokenPair, err := h.jwtManager.GenerateTokenPair(user.ID, user.Username, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to generate tokens",
        })
        return
    }
    
    // Update last login
    if err := h.userRepo.UpdateLastLogin(user.ID); err != nil {
        // Log error but don't fail the request
        // logger.Error("Failed to update last login", err)
    }
    
    // Set HTTP-only cookie for refresh token if remember me is checked
    if req.Remember {
        c.SetSameSite(http.SameSiteLaxMode)
        c.SetCookie(
            "refresh_token",
            tokenPair.RefreshToken,
            int(time.Hour.Seconds()*24*30), // 30 days
            "/",
            "",
            true,  // Secure (HTTPS only)
            true,  // HTTP only
        )
    }
    
    c.JSON(http.StatusOK, gin.H{
        "message": "Login successful",
        "user": gin.H{
            "id":        user.ID,
            "username":  user.Username,
            "email":     user.Email,
            "full_name": user.FullName,
            "role":      user.Role,
        },
        "tokens": tokenPair,
    })
}

// Register creates a new user account
func (h *AuthHandler) Register(c *gin.Context) {
    var req RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error":   "Invalid input",
            "details": err.Error(),
        })
        return
    }
    
    // Validate password confirmation
    if req.Password != req.ConfirmPassword {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Passwords do not match",
        })
        return
    }
    
    // Check if user already exists
    if existingUser, _ := h.userRepo.GetByUsername(req.Username); existingUser != nil {
        c.JSON(http.StatusConflict, gin.H{
            "error": "Username already exists",
        })
        return
    }
    
    if existingUser, _ := h.userRepo.GetByEmail(req.Email); existingUser != nil {
        c.JSON(http.StatusConflict, gin.H{
            "error": "Email already exists",
        })
        return
    }
    
    // Hash password
    hashedPassword, err := h.passwordHasher.HashPassword(req.Password)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to process password",
        })
        return
    }
    
    // Create user
    user := &models.User{
        Username: req.Username,
        Email:    req.Email,
        FullName: req.FullName,
        Password: hashedPassword,
        Role:     "user",
        Active:   true,
    }
    
    if err := h.userRepo.Create(user); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to create user",
        })
        return
    }
    
    c.JSON(http.StatusCreated, gin.H{
        "message": "User created successfully",
        "user": gin.H{
            "id":        user.ID,
            "username":  user.Username,
            "email":     user.Email,
            "full_name": user.FullName,
            "role":      user.Role,
        },
    })
}

// RefreshToken generates new access token using refresh token
func (h *AuthHandler) RefreshToken(c *gin.Context) {
    // Try to get refresh token from cookie first
    refreshToken, err := c.Cookie("refresh_token")
    if err != nil {
        // If not in cookie, try Authorization header
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Refresh token required",
            })
            return
        }
        
        tokenParts := strings.Split(authHeader, " ")
        if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid token format",
            })
            return
        }
        refreshToken = tokenParts[1]
    }
    
    // Generate new token pair
    tokenPair, err := h.jwtManager.RefreshAccessToken(refreshToken)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Invalid refresh token",
        })
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "message": "Token refreshed successfully",
        "tokens":  tokenPair,
    })
}

// Logout invalidates user session
func (h *AuthHandler) Logout(c *gin.Context) {
    // Clear refresh token cookie
    c.SetCookie(
        "refresh_token",
        "",
        -1,
        "/",
        "",
        true,
        true,
    )
    
    c.JSON(http.StatusOK, gin.H{
        "message": "Logged out successfully",
    })
}

// GetProfile returns current user profile
func (h *AuthHandler) GetProfile(c *gin.Context) {
    userInterface, exists := c.Get("user")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "User not found in context",
        })
        return
    }
    
    claims, ok := userInterface.(*auth.Claims)
    if !ok {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Invalid user data",
        })
        return
    }
    
    user, err := h.userRepo.GetByID(claims.UserID)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "User not found",
        })
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "user": gin.H{
            "id":         user.ID,
            "username":   user.Username,
            "email":      user.Email,
            "full_name":  user.FullName,
            "role":       user.Role,
            "last_login": user.LastLogin,
            "created_at": user.CreatedAt,
        },
    })
}

Example 2: OAuth2 Integration with Google

// auth/oauth.go
package auth

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
    
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

// OAuthConfig holds OAuth configuration
type OAuthConfig struct {
    GoogleClientID     string
    GoogleClientSecret string
    RedirectURL       string
}

// OAuthManager handles OAuth operations
type OAuthManager struct {
    config      OAuthConfig
    googleOAuth *oauth2.Config
}

// GoogleUserInfo represents user info from Google
type GoogleUserInfo struct {
    ID            string `json:"id"`
    Email         string `json:"email"`
    VerifiedEmail bool   `json:"verified_email"`
    Name          string `json:"name"`
    GivenName     string `json:"given_name"`
    FamilyName    string `json:"family_name"`
    Picture       string `json:"picture"`
    Locale        string `json:"locale"`
}

// NewOAuthManager creates a new OAuth manager
func NewOAuthManager(config OAuthConfig) *OAuthManager {
    googleOAuth := &oauth2.Config{
        ClientID:     config.GoogleClientID,
        ClientSecret: config.GoogleClientSecret,
        RedirectURL:  config.RedirectURL,
        Scopes: []string{
            "https://www.googleapis.com/auth/userinfo.email",
            "https://www.googleapis.com/auth/userinfo.profile",
        },
        Endpoint: google.Endpoint,
    }
    
    return &OAuthManager{
        config:      config,
        googleOAuth: googleOAuth,
    }
}

// GetGoogleAuthURL returns the Google OAuth URL
func (o *OAuthManager) GetGoogleAuthURL(state string) string {
    return o.googleOAuth.AuthCodeURL(state, oauth2.AccessTypeOffline)
}

// ExchangeGoogleCode exchanges authorization code for user info
func (o *OAuthManager) ExchangeGoogleCode(code string) (*GoogleUserInfo, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    token, err := o.googleOAuth.Exchange(ctx, code)
    if err != nil {
        return nil, fmt.Errorf("failed to exchange code: %w", err)
    }
    
    // Get user info from Google
    client := o.googleOAuth.Client(ctx, token)
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/oauth2/v2/userinfo", nil)
    if err != nil { return nil, err }
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("failed to get user info: %w", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("Google userinfo returned HTTP %d", resp.StatusCode)
    }
    body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
    if err != nil {
        return nil, fmt.Errorf("failed to read response: %w", err)
    }
    
    var userInfo GoogleUserInfo
    if err := json.Unmarshal(body, &userInfo); err != nil {
        return nil, fmt.Errorf("failed to parse user info: %w", err)
    }
    if userInfo.ID == "" || userInfo.Email == "" || !userInfo.VerifiedEmail {
        return nil, fmt.Errorf("Google did not return a verified email and user ID")
    }
    
    return &userInfo, nil
}

// handlers/oauth_handler.go
package handlers

import (
    "crypto/rand"
    "encoding/hex"
    "net/http"
    "time"
    
    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"
    "github.com/your-app/auth"
    "github.com/your-app/models"
    "github.com/your-app/repositories"
)

// OAuthHandler handles OAuth authentication
type OAuthHandler struct {
    userRepo      repositories.UserRepository
    oauthManager  *auth.OAuthManager
    jwtManager    *auth.JWTManager
    passwordHasher *auth.PasswordHasher
}

// NewOAuthHandler creates a new OAuth handler
func NewOAuthHandler(
    userRepo repositories.UserRepository,
    oauthManager *auth.OAuthManager,
    jwtManager *auth.JWTManager,
) *OAuthHandler {
    return &OAuthHandler{
        userRepo:       userRepo,
        oauthManager:   oauthManager,
        jwtManager:     jwtManager,
        passwordHasher: auth.NewPasswordHasher(),
    }
}

// GoogleAuth initiates Google OAuth flow
func (h *OAuthHandler) GoogleAuth(c *gin.Context) {
    // Generate state parameter for CSRF protection
    state, err := generateRandomString(32)
    if err != nil { c.Status(http.StatusInternalServerError); return }
    
    // Store state in session
    session := sessions.Default(c)
    session.Set("oauth_state", state)
    if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    
    // Redirect to Google OAuth
    authURL := h.oauthManager.GetGoogleAuthURL(state)
    c.Redirect(http.StatusTemporaryRedirect, authURL)
}

// GoogleCallback handles Google OAuth callback
func (h *OAuthHandler) GoogleCallback(c *gin.Context) {
    // Verify state parameter
    session := sessions.Default(c)
    storedState := session.Get("oauth_state")
    if storedState == nil || storedState != c.Query("state") {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Invalid state parameter",
        })
        return
    }
    
    // Clear state from session
    session.Delete("oauth_state")
    if err := session.Save(); err != nil { c.Status(http.StatusInternalServerError); return }
    
    // Exchange code for user info
    code := c.Query("code")
    if code == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Authorization code required",
        })
        return
    }
    
    userInfo, err := h.oauthManager.ExchangeGoogleCode(code)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to get user information",
        })
        return
    }
    
    // Check if user exists
    user, err := h.userRepo.GetByEmail(userInfo.Email)
    if err != nil {
        // User doesn't exist, create new account
        username, err := generateUsernameFromEmail(userInfo.Email)
        if err != nil { c.Status(http.StatusInternalServerError); return }
        password, err := generateRandomString(32)
        if err != nil { c.Status(http.StatusInternalServerError); return }
        user = &models.User{
            Username: username,
            Email:    userInfo.Email,
            FullName: userInfo.Name,
            Role:     "user",
            Active:   true,
            // Generate random password for OAuth users
            Password: password,
        }
        
        // Hash the generated password
        hashedPassword, err := h.passwordHasher.HashPassword(user.Password)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Failed to create user account",
            })
            return
        }
        user.Password = hashedPassword
        
        if err := h.userRepo.Create(user); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Failed to create user account",
            })
            return
        }
    }
    
    // Generate JWT tokens
    tokenPair, err := h.jwtManager.GenerateTokenPair(user.ID, user.Username, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "Failed to generate tokens",
        })
        return
    }
    
    // Update last login
    h.userRepo.UpdateLastLogin(user.ID)
    
    // Set tokens as HTTPS-only cookies for a same-origin web application.
    // Cookie-authenticated state-changing endpoints also need CSRF protection.
    c.SetSameSite(http.SameSiteLaxMode)
    c.SetCookie(
        "refresh_token",
        tokenPair.RefreshToken,
        int(time.Hour.Seconds()*24*30), // 30 days
        "/",
        "",
        true,  // Secure
        true,  // HTTP only
    )
    
    c.SetCookie("access_token", tokenPair.AccessToken,
        int(time.Until(tokenPair.ExpiresAt).Seconds()), "/", "", true, true)
    // The bearer-header middleware below does not read cookies: add a
    // separate cookie-auth middleware with CSRF checks before using this flow.
    // Never put a bearer token in a URL (logs, browser history and Referer leak it).
    // Serve the frontend on the same origin; for a separate SPA origin, design a
    // server-side code exchange instead of redirecting with an access token.
    c.Redirect(http.StatusSeeOther, "/")
}

// generateRandomString creates a random string of specified length
func generateRandomString(length int) (string, error) {
    bytes := make([]byte, length)
    if _, err := rand.Read(bytes); err != nil { return "", err }
    return hex.EncodeToString(bytes)[:length], nil
}

// generateUsernameFromEmail creates a username from email
func generateUsernameFromEmail(email string) (string, error) {
    parts := strings.Split(email, "@")
    username := parts[0]
    
    // Add random suffix to avoid conflicts
    suffix, err := generateRandomString(16) // 64 bits of random suffix
    if err != nil { return "", err }
    return username + "_" + suffix, nil
}

Example 3: Role-Based Access Control System

// auth/rbac.go
package auth

import (
    "fmt"
    "strings"
)

// Permission represents a system permission
type Permission struct {
    ID       uint   `json:"id" gorm:"primaryKey"`
    Name     string `json:"name" gorm:"uniqueIndex;not null;size:50"`
    Resource string `json:"resource" gorm:"not null;size:50"`
    Action   string `json:"action" gorm:"not null;size:20"`
    Description string `json:"description" gorm:"size:200"`
}

// Role represents a user role
type Role struct {
    ID          uint         `json:"id" gorm:"primaryKey"`
    Name        string       `json:"name" gorm:"uniqueIndex;not null;size:50"`
    Description string       `json:"description" gorm:"size:200"`
    Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
}

// UserRole links users to roles
type UserRole struct {
    UserID uint `json:"user_id" gorm:"primaryKey"`
    RoleID uint `json:"role_id" gorm:"primaryKey"`
}

// RBAC manages role-based access control
type RBAC struct {
    permissions map[string]Permission
    roles       map[string]Role
    userRoles   map[uint][]Role
}

// NewRBAC creates a new RBAC instance
func NewRBAC() *RBAC {
    return &RBAC{
        permissions: make(map[string]Permission),
        roles:       make(map[string]Role),
        userRoles:   make(map[uint][]Role),
    }
}

// AddPermission adds a permission to the system
func (r *RBAC) AddPermission(name, resource, action, description string) {
    permission := Permission{
        Name:        name,
        Resource:    resource,
        Action:      action,
        Description: description,
    }
    r.permissions[name] = permission
}

// AddRole adds a role to the system
func (r *RBAC) AddRole(name, description string, permissionNames []string) {
    role := Role{
        Name:        name,
        Description: description,
        Permissions: make([]Permission, 0),
    }
    
    // Add permissions to role
    for _, permName := range permissionNames {
        if perm, exists := r.permissions[permName]; exists {
            role.Permissions = append(role.Permissions, perm)
        }
    }
    
    r.roles[name] = role
}

// AssignRole assigns a role to a user
func (r *RBAC) AssignRole(userID uint, roleName string) {
    if role, exists := r.roles[roleName]; exists {
        if _, userExists := r.userRoles[userID]; !userExists {
            r.userRoles[userID] = make([]Role, 0)
        }
        r.userRoles[userID] = append(r.userRoles[userID], role)
    }
}

// HasPermission checks if user has specific permission
func (r *RBAC) HasPermission(userID uint, resource, action string) bool {
    userRoles, exists := r.userRoles[userID]
    if !exists {
        return false
    }
    
    for _, role := range userRoles {
        for _, permission := range role.Permissions {
            if permission.Resource == resource && permission.Action == action {
                return true
            }
        }
    }
    
    return false
}

// HasRole checks if user has specific role
func (r *RBAC) HasRole(userID uint, roleName string) bool {
    userRoles, exists := r.userRoles[userID]
    if !exists {
        return false
    }
    
    for _, role := range userRoles {
        if role.Name == roleName {
            return true
        }
    }
    
    return false
}

// GetUserPermissions returns all permissions for a user
func (r *RBAC) GetUserPermissions(userID uint) []string {
    var permissions []string
    permissionSet := make(map[string]bool)
    
    userRoles, exists := r.userRoles[userID]
    if !exists {
        return permissions
    }
    
    for _, role := range userRoles {
        for _, permission := range role.Permissions {
            permKey := fmt.Sprintf("%s:%s", permission.Resource, permission.Action)
            if !permissionSet[permKey] {
                permissions = append(permissions, permKey)
                permissionSet[permKey] = true
            }
        }
    }
    
    return permissions
}

// InitializeDefaultRoles sets up default system roles
func (r *RBAC) InitializeDefaultRoles() {
    // Define permissions
    r.AddPermission("read_users", "users", "read", "View user information")
    r.AddPermission("write_users", "users", "write", "Create and update users")
    r.AddPermission("delete_users", "users", "delete", "Delete users")
    r.AddPermission("read_posts", "posts", "read", "View posts")
    r.AddPermission("write_posts", "posts", "write", "Create and update posts")
    r.AddPermission("delete_posts", "posts", "delete", "Delete posts")
    r.AddPermission("read_comments", "comments", "read", "View comments")
    r.AddPermission("write_comments", "comments", "write", "Create and update comments")
    r.AddPermission("delete_comments", "comments", "delete", "Delete comments")
    r.AddPermission("admin_panel", "admin", "access", "Access admin panel")
    
    // Define roles
    r.AddRole("user", "Regular user", []string{
        "read_posts", "write_posts", "read_comments", "write_comments",
    })
    
    r.AddRole("moderator", "Content moderator", []string{
        "read_posts", "write_posts", "delete_posts",
        "read_comments", "write_comments", "delete_comments",
        "read_users",
    })
    
    r.AddRole("admin", "System administrator", []string{
        "read_users", "write_users", "delete_users",
        "read_posts", "write_posts", "delete_posts",
        "read_comments", "write_comments", "delete_comments",
        "admin_panel",
    })
}

// middleware/auth_middleware.go
package middleware

import (
    "net/http"
    "strings"
    
    "github.com/gin-gonic/gin"
    "github.com/your-app/auth"
)

// AuthMiddleware validates JWT tokens
func AuthMiddleware(jwtManager *auth.JWTManager) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Authorization header required",
            })
            c.Abort()
            return
        }
        
        tokenParts := strings.Split(authHeader, " ")
        if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid authorization header format",
            })
            c.Abort()
            return
        }
        
        token := tokenParts[1]
        claims, err := jwtManager.ValidateToken(token)
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid or expired token",
            })
            c.Abort()
            return
        }
        
        if claims.TokenType != "access" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid token type",
            })
            c.Abort()
            return
        }
        
        c.Set("user", claims)
        c.Next()
    }
}

// RequirePermission middleware checks user permissions
func RequirePermission(rbac *auth.RBAC, resource, action string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userInterface, exists := c.Get("user")
        if !exists {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "User not authenticated",
            })
            c.Abort()
            return
        }
        
        claims, ok := userInterface.(*auth.Claims)
        if !ok {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Invalid user data",
            })
            c.Abort()
            return
        }
        
        if !rbac.HasPermission(claims.UserID, resource, action) {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "Insufficient permissions",
                "required": fmt.Sprintf("%s:%s", resource, action),
            })
            c.Abort()
            return
        }
        
        c.Next()
    }
}

// RequireRole middleware checks user roles
func RequireRole(rbac *auth.RBAC, roleName string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userInterface, exists := c.Get("user")
        if !exists {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "User not authenticated",
            })
            c.Abort()
            return
        }
        
        claims, ok := userInterface.(*auth.Claims)
        if !ok {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Invalid user data",
            })
            c.Abort()
            return
        }
        
        if !rbac.HasRole(claims.UserID, roleName) {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "Insufficient role",
                "required": roleName,
            })
            c.Abort()
            return
        }
        
        c.Next()
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Multi-Factor Authentication (15 minutes)

Objective: Implement TOTP-based two-factor authentication

Instructions:

  1. Add MFA models and database fields
  2. Implement TOTP generation and validation
  3. Create MFA setup and verification endpoints
  4. Modify login flow to require MFA for enabled users
  5. Add MFA recovery codes

Starter Code:

// Add to user model
type User struct {
    // ... existing fields
    MFAEnabled    bool   `json:"mfa_enabled" gorm:"default:false"`
    MFASecret     string `json:"-" gorm:"size:32"`
    RecoveryCodes []string `json:"-" gorm:"type:json"`
}

// TODO: Implement MFA functionality
func setupMFA(userID uint) (string, []string, error) {
    // Generate TOTP secret
    // Generate recovery codes
    // Update user record
}

func verifyMFA(userID uint, code string) bool {
    // Verify TOTP code or recovery code
}

Solution:

// auth/mfa.go
package auth

import (
    "crypto/rand"
    "encoding/base32"
    "fmt"
    "time"
    
    "github.com/pquerna/otp"
    "github.com/pquerna/otp/totp"
)

// MFAManager handles multi-factor authentication
type MFAManager struct {
    issuer string
}

// NewMFAManager creates a new MFA manager
func NewMFAManager(issuer string) *MFAManager {
    return &MFAManager{issuer: issuer}
}

// GenerateSecret creates a new TOTP secret
func (m *MFAManager) GenerateSecret(accountName string) (*otp.Key, error) {
    return totp.Generate(totp.GenerateOpts{
        Issuer:      m.issuer,
        AccountName: accountName,
        Period:      30,
        Digits:      otp.DigitsSix,
        Algorithm:   otp.AlgorithmSHA1,
    })
}

// ValidateCode validates a TOTP code
func (m *MFAManager) ValidateCode(secret, code string) bool {
    return totp.Validate(code, secret)
}

// GenerateRecoveryCodes creates backup recovery codes
func (m *MFAManager) GenerateRecoveryCodes() ([]string, error) {
    codes := make([]string, 10)
    for i := 0; i < 10; i++ {
        bytes := make([]byte, 5)
        if _, err := rand.Read(bytes); err != nil {
            return nil, err
        }
        codes[i] = base32.StdEncoding.EncodeToString(bytes)
    }
    return codes, nil
}

// MFA endpoints in auth handler
func (h *AuthHandler) SetupMFA(c *gin.Context) {
    claims := getUserClaims(c)
    
    user, err := h.userRepo.GetByID(claims.UserID)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    
    if user.MFAEnabled {
        c.JSON(http.StatusBadRequest, gin.H{"error": "MFA already enabled"})
        return
    }
    
    // Generate secret and recovery codes
    key, err := h.mfaManager.GenerateSecret(user.Email)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate secret"})
        return
    }
    
    recoveryCodes, err := h.mfaManager.GenerateRecoveryCodes()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate recovery codes"})
        return
    }
    
    // Store secret temporarily (not enabled until verified)
    user.MFASecret = key.Secret()
    user.RecoveryCodes = recoveryCodes
    h.userRepo.Update(user)
    
    c.JSON(http.StatusOK, gin.H{
        "secret":         key.Secret(),
        "qr_code":        key.String(),
        "recovery_codes": recoveryCodes,
        "message":        "Please verify MFA setup with a code from your authenticator app",
    })
}

func (h *AuthHandler) VerifyMFA(c *gin.Context) {
    var req struct {
        Code string `json:"code" binding:"required"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    claims := getUserClaims(c)
    user, err := h.userRepo.GetByID(claims.UserID)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    
    if !h.mfaManager.ValidateCode(user.MFASecret, req.Code) {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid MFA code"})
        return
    }
    
    // Enable MFA
    user.MFAEnabled = true
    h.userRepo.Update(user)
    
    c.JSON(http.StatusOK, gin.H{
        "message": "MFA enabled successfully",
    })
}

Exercise 2: API Key Authentication (15 minutes)

Objective: Implement API key-based authentication for external services

Requirements:

  • Generate and manage API keys for users
  • Support different access levels (read-only, read-write, admin)
  • Implement rate limiting per API key
  • Add API key rotation and expiration
  • Log API key usage for monitoring

Hints:

  • Store API keys securely with proper hashing
  • Use middleware to validate API keys from headers
  • Implement key rotation without breaking existing integrations

Exercise 3: Advanced Security Features (10 minutes)

Objective: Add comprehensive security measures

Challenge: Implement:

  • Account lockout after failed login attempts
  • Password reset with secure tokens
  • Email verification for new accounts
  • Suspicious activity detection and alerts
  • CAPTCHA integration for brute force prevention

Extension Ideas:

  • Add geolocation-based login alerts
  • Implement device fingerprinting
  • Create security audit logs
  • Add progressive authentication (step-up auth)

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: JWT tokens not expiring properly Cause: Not validating expiration claims or using incorrect time zones Solution: Always validate exp claim and use UTC for consistency

  2. Problem: Refresh token security vulnerabilities Cause: Storing refresh tokens insecurely or not rotating them Solution: Use HTTP-only cookies for web apps, rotate refresh tokens on use

  3. Problem: OAuth state parameter vulnerabilities Cause: Not validating state parameter or using predictable values Solution: Generate cryptographically secure random state values and validate them

Debugging Tips

  • Log authentication events for security monitoring
  • Use token debugging tools to inspect JWT claims
  • Test authentication flows with different scenarios (success, failure, edge cases)
  • Monitor rate limiting and brute force protection effectiveness

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Use strong password requirements and validation
  • Implement proper session timeout and management
  • Apply the principle of least privilege for authorization
  • Use HTTPS for all authentication-related communications
  • Regularly audit and rotate secrets and keys

Next Steps in Your Learning Journey

  • Learn about advanced authentication methods (WebAuthn, biometrics)
  • Explore enterprise authentication systems (SAML, LDAP, Active Directory)
  • Study security frameworks and compliance requirements (OWASP, SOC 2)
  • Investigate advanced authorization patterns (ABAC, PBAC)
  • Practice security testing and penetration testing techniques

9. Summary and Review (5 minutes)

Key Takeaways

  1. Authentication and authorization are distinct but complementary security measures
  2. JWT tokens provide stateless authentication but require careful implementation
  3. OAuth integration enables social logins while maintaining security
  4. RBAC systems provide flexible and scalable permission management

Quick Review Questions

  1. What’s the difference between authentication and authorization?
  2. How do you securely implement JWT refresh tokens?
  3. What security measures should be implemented for OAuth flows?
  4. How does role-based access control improve security?

For Instructors

Teaching Tips

  • Demonstrate both secure and insecure implementations to show vulnerabilities
  • Use real-world examples of authentication breaches and their consequences
  • Show how to test authentication systems with various tools
  • Encourage students to think like attackers to identify weaknesses
  • Emphasize the importance of security by design, not as an afterthought

Assessment Ideas

  • Build a complete authentication system with multiple methods
  • Implement a comprehensive RBAC system for a specific domain
  • Create security tests that verify authentication and authorization
  • Design an authentication flow for a specific business requirement

Adaptation Notes

  • For beginners: Focus on basic JWT and session authentication before advanced topics
  • For advanced students: Introduce enterprise authentication patterns and security frameworks
  • Online delivery: Provide testing tools and security scanning utilities
  • Time management: OAuth and RBAC sections can be extended for more complex scenarios