Skip to main content
This recipe shows how to create robust API key authentication middleware for the Gin web framework.

Complete Middleware Implementation

package main

import (
    "net/http"
    "os"
    "slices"
    "strconv"
    "strings"

    "github.com/gin-gonic/gin"
    unkey "github.com/unkeyed/sdks/api/go/v2"
    "github.com/unkeyed/sdks/api/go/v2/models/components"
)

var unkeyClient *unkey.Unkey

func init() {
    unkeyClient = unkey.New(
        unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")),
    )
}

// UnkeyAuth creates a Gin middleware for API key verification
func UnkeyAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Missing Authorization header",
                "code":  "MISSING_KEY",
            })
            return
        }

        apiKey := strings.TrimPrefix(authHeader, "Bearer ")

        // Verify with Unkey
        res, err := unkeyClient.Keys.VerifyKey(c.Request.Context(), components.V2KeysVerifyKeyRequestBody{
            Key: apiKey,
        })

        if err != nil {
            c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
                "error":   "Verification service unavailable",
                "code":    "SERVICE_ERROR",
                "message": err.Error(),
            })
            return
        }

        if !res.V2KeysVerifyKeyResponseBody.Data.Valid {
            code := string(res.V2KeysVerifyKeyResponseBody.Data.Code)

            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Invalid API key",
                "code":  code,
            })
            return
        }

        // Store verification result in context (as pointer for type assertion compatibility)
        c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data)
        c.Next()
    }
}

// RequirePermission creates middleware to check specific permissions
func RequirePermission(permission string) gin.HandlerFunc {
    return func(c *gin.Context) {
        result, exists := c.Get("unkey")
        if !exists {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Authentication required",
                "code":  "AUTH_REQUIRED",
            })
            return
        }

        keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData)
        if !ok || keyResult == nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "error": "Invalid authentication context",
                "code":  "INTERNAL_ERROR",
            })
            return
        }

        if slices.Contains(keyResult.Permissions, permission) {
            c.Next()
            return
        }

        c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
            "error":    "Insufficient permissions",
            "code":     "FORBIDDEN",
            "required": permission,
        })
    }
}

// RequireRole creates middleware to check specific roles
func RequireRole(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        result, exists := c.Get("unkey")
        if !exists {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Authentication required",
                "code":  "AUTH_REQUIRED",
            })
            return
        }

        keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData)
        if !ok || keyResult == nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "error": "Invalid authentication context",
                "code":  "INTERNAL_ERROR",
            })
            return
        }

        if slices.Contains(keyResult.Roles, role) {
            c.Next()
            return
        }

        c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
            "error":    "Insufficient role",
            "code":     "FORBIDDEN",
            "required": role,
        })
    }
}

// GetUnkeyResult retrieves the Unkey verification result from context
func GetUnkeyResult(c *gin.Context) *components.V2KeysVerifyKeyResponseData {
    result, ok := c.Get("unkey")
    if !ok {
        return nil
    }
    r, ok := result.(*components.V2KeysVerifyKeyResponseData)
    if !ok {
        return nil
    }
    return r
}

// Usage example
func main() {
    r := gin.Default()

    // Public routes
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Protected API group
    api := r.Group("/api")
    api.Use(UnkeyAuth())
    {
        api.GET("/data", func(c *gin.Context) {
            result := GetUnkeyResult(c)
            ownerID := ""
            if result.Identity != nil {
                ownerID = result.Identity.ExternalID
            }
            c.JSON(http.StatusOK, gin.H{
                "message": "Access granted",
                "key_id":  *result.KeyID,
                "owner":   ownerID,
                "meta":    result.Meta,
            })
        })

        api.GET("/profile", func(c *gin.Context) {
            result := GetUnkeyResult(c)
            c.JSON(http.StatusOK, gin.H{
                "key_id": *result.KeyID,
                "permissions": result.Permissions,
                "roles": result.Roles,
            })
        })
    }

    // Admin routes with permission check
    admin := r.Group("/api/admin")
    admin.Use(UnkeyAuth(), RequirePermission("admin:read"))
    {
        admin.GET("/users", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "message": "Admin access granted",
                "users":   []string{"user1", "user2"},
            })
        })

        admin.POST("/config", RequirePermission("admin:write"), func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "message": "Config updated",
            })
        })
    }

    r.Run(":8080")
}

Rate Limiting Integration

Combine with Unkey’s rate limiting:
func RateLimitMiddleware(namespace string) gin.HandlerFunc {
    return func(c *gin.Context) {
        result := GetUnkeyResult(c)

        if result == nil {
            c.Next()
            return
        }

        // Use the key's built-in rate limits from verification
        // These are already checked during key verification

        // Or use standalone rate limit API for custom limits
        res, err := unkeyClient.Ratelimits.Limit(c.Request.Context(), components.V2RatelimitsLimitRequestBody{
            Namespace:  namespace,
            Identifier: *result.KeyID,
            Limit:      100,
            Duration:   60000, // 100 per minute
        })

        if err != nil {
            c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
                "error": "Rate limit check failed",
            })
            return
        }

        if !res.V2RatelimitsLimitResponseBody.Success {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                "error":   "Rate limit exceeded",
                "reset":   res.V2RatelimitsLimitResponseBody.Reset,
                "limit":   100,
                "window":  "60s",
            })
            return
        }

        // Add rate limit headers
        c.Header("X-RateLimit-Limit", "100")
        c.Header("X-RateLimit-Remaining", strconv.FormatInt(res.V2RatelimitsLimitResponseBody.Remaining, 10))

        c.Next()
    }
}

Testing

# Test without key
curl http://localhost:8080/api/data
# {"error":"Missing Authorization header","code":"MISSING_KEY"}

# Test with valid key
curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/data
# {"message":"Access granted","key_id":"key_..."}

# Test admin route without permission
curl -H "Authorization: Bearer USER_KEY" http://localhost:8080/api/admin/users
# {"error":"Insufficient permissions","code":"FORBIDDEN"}

# Test admin route with permission
curl -H "Authorization: Bearer ADMIN_KEY" http://localhost:8080/api/admin/users
# {"message":"Admin access granted","users":["user1","user2"]}

Go Quickstart

Get started with Go and Unkey

Go SDK Reference

Complete Go SDK documentation
Last modified on February 17, 2026