Skip to content

Context Helpers

The Context Helpers package provides type-safe, reusable functions for extracting user authentication information from Gin request contexts. This eliminates repetitive boilerplate code in handlers and provides a clean, maintainable approach to user authentication and authorization.

๐ŸŽฏ Overview

The Context Helpers package (internal/ctx) offers a comprehensive set of functions that simplify user authentication and authorization in your handlers. Instead of manually extracting and validating JWT claims in every protected endpoint, you can use these helper functions for clean, readable, and maintainable code.

Key Benefits

  • ๐Ÿ”„ DRY Principle: Eliminates code duplication across handlers
  • ๐Ÿ›ก๏ธ Type Safety: Built-in type assertions with proper error handling
  • ๐Ÿงช Testability: Comprehensive test coverage ensures reliability
  • ๐Ÿ“– Readability: Clean, self-documenting authentication code
  • โšก Performance: Optimized context extraction with minimal overhead

๐Ÿ“ฆ Available Functions

User Information Extraction

GetUser(c *gin.Context) *auth.Claims

Retrieves the complete authenticated user claims from context.

claims := ctx.GetUser(c)
if claims != nil {
    // User is authenticated
    fmt.Printf("User ID: %d, Email: %s\n", claims.UserID, claims.Email)
}

MustGetUser(c *gin.Context) (*auth.Claims, error)

Retrieves user claims or returns an error if not found.

claims, err := ctx.MustGetUser(c)
if err != nil {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    return
}
// Use claims safely

GetUserID(c *gin.Context) uint

Extracts the authenticated user's ID from context. Returns 0 if not found.

userID := ctx.GetUserID(c)
if userID == 0 {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    return
}

MustGetUserID(c *gin.Context) (uint, error)

Gets user ID with error handling.

userID, err := ctx.MustGetUserID(c)
if err != nil {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    return
}

GetEmail(c *gin.Context) string

Extracts the authenticated user's email address.

email := ctx.GetEmail(c)
if email == "" {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    return
}

GetUserName(c *gin.Context) string

Extracts the authenticated user's name.

userName := ctx.GetUserName(c)
fmt.Printf("Welcome, %s!\n", userName)

Authentication & Authorization

IsAuthenticated(c *gin.Context) bool

Checks if the request has valid authentication.

if !ctx.IsAuthenticated(c) {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    return
}

CanAccessUser(c *gin.Context, targetUserID uint) bool

Checks if the authenticated user can access the target user's resources (ownership-based access control).

if !ctx.CanAccessUser(c, uint(id)) {
    c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
    return
}

HasRole(c *gin.Context, role string) bool

Checks if the user has a specific role (placeholder for future RBAC implementation).

if !ctx.HasRole(c, "admin") {
    c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
    return
}

Future RBAC Support

The HasRole function is currently a placeholder that returns false. It will be implemented when role-based access control is added to the system.

๐Ÿ”„ Code Transformation

Before: Repetitive Boilerplate

Every protected handler required verbose authentication code:

func (h *Handler) GetUser(c *gin.Context) {
    // Parse ID from URL
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
        return
    }

    // Extract user claims manually
    claims, exists := c.Get("user")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
        return
    }
    userClaims := claims.(*auth.Claims)

    // Authorization check
    if userClaims.UserID != uint(id) {
        c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
        return
    }

    // Actual handler logic...
}

After: Clean & Type-Safe

With Context Helpers, the same functionality becomes clean and readable:

func (h *Handler) GetUser(c *gin.Context) {
    // Parse ID from URL
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
        return
    }

    // Clean authorization check
    if !ctx.CanAccessUser(c, uint(id)) {
        c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
        return
    }

    // Actual handler logic...
}

๐Ÿ“Š Impact Metrics

Metric Before After Improvement
Lines per handler 5-6 lines 1 line 83% reduction
Type safety Manual assertions Built-in 100% safe
Test coverage None 25+ tests Complete coverage
Code duplication High Eliminated DRY principle

๐Ÿ› ๏ธ Usage Examples

Basic Authentication Check

func (h *Handler) ProtectedEndpoint(c *gin.Context) {
    if !ctx.IsAuthenticated(c) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
        return
    }

    userID := ctx.GetUserID(c)
    // Use userID safely
}

Resource Ownership Validation

func (h *Handler) UpdateUser(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
        return
    }

    // Check if user can access this resource
    if !ctx.CanAccessUser(c, uint(id)) {
        c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
        return
    }

    // Proceed with update...
}

User Information Display

func (h *Handler) GetProfile(c *gin.Context) {
    user := ctx.GetUser(c)
    if user == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
        return
    }

    profile := gin.H{
        "id":    user.UserID,
        "email": user.Email,
        "name":  user.Name,
    }

    c.JSON(http.StatusOK, profile)
}

Error Handling with Must Functions

func (h *Handler) StrictEndpoint(c *gin.Context) {
    userID, err := ctx.MustGetUserID(c)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
        return
    }

    email, err := ctx.MustGetUser(c)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "User information missing"})
        return
    }

    // Use userID and email safely
}

๐Ÿงช Testing

The Context Helpers package includes comprehensive test coverage with 25+ unit tests covering:

  • โœ… Happy path scenarios: Valid user extraction
  • โœ… Edge cases: Missing authentication, invalid types
  • โœ… Error conditions: Malformed context data
  • โœ… Authorization logic: Access control validation
  • โœ… Type safety: Proper type assertions

Running Tests

# Run all tests
go test ./tests/context_test.go -v

# Run with coverage
go test ./tests/context_test.go -v -cover

# Run all project tests
make test

๐Ÿ”ง Integration

Prerequisites

The Context Helpers require:

  1. JWT Authentication Middleware: Must be applied to protected routes
  2. Auth Claims Structure: Compatible with internal/auth.Claims
  3. Context Key: Uses auth.KeyUser constant for context storage

Middleware Setup

Ensure your routes are protected with the authentication middleware:

// In your router setup
authMiddleware := auth.NewMiddleware(authService)
protected := router.Group("/api/v1")
protected.Use(authMiddleware)

// Now handlers can use context helpers
protected.GET("/users/:id", userHandler.GetUser)

๐Ÿš€ Best Practices

1. Use Appropriate Functions

  • Safe functions (GetUserID, GetEmail) for optional user info
  • Must functions (MustGetUserID, MustGetUser) when authentication is required
  • Boolean checks (IsAuthenticated, CanAccessUser) for conditional logic

2. Consistent Error Handling

// Good: Consistent error responses
if !ctx.IsAuthenticated(c) {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
    return
}

// Good: Use appropriate HTTP status codes
if !ctx.CanAccessUser(c, targetID) {
    c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
    return
}

3. Combine with Validation

func (h *Handler) UpdateUser(c *gin.Context) {
    // Input validation
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
        return
    }

    // Authentication & authorization
    if !ctx.CanAccessUser(c, uint(id)) {
        c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
        return
    }

    // Business logic validation
    var req UpdateRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // Proceed with update...
}

๐Ÿ”ฎ Future Enhancements

The Context Helpers package is designed for extensibility:

  • Role-Based Access Control (RBAC): HasRole function ready for implementation
  • Permission System: Extensible for complex permission checks
  • Multi-tenant Support: Ready for tenant-based access control
  • Audit Logging: Easy integration with user context information

The Context Helpers package transforms authentication code from repetitive boilerplate into clean, maintainable, and testable functions. By eliminating code duplication and providing type-safe user extraction, it significantly improves developer experience and code quality.