This recipe shows how to create robust API key authentication middleware for the Gin web framework.
Complete Middleware Implementation#
package mainimport ( "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.Unkeyfunc init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), )}// UnkeyAuth creates a Gin middleware for API key verificationfunc 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 permissionsfunc 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 rolesfunc 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 contextfunc 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 examplefunc 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) identityExternalID := "" if result.Identity != nil { identityExternalID = result.Identity.ExternalID } c.JSON(http.StatusOK, gin.H{ "message": "Access granted", "key_id": *result.KeyID, "identity_external_id": identityExternalID, "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 keycurl http://localhost:8080/api/data# {"error":"Missing Authorization header","code":"MISSING_KEY"}# Test with valid keycurl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/data# {"message":"Access granted","key_id":"key_..."}# Test admin route without permissioncurl -H "Authorization: Bearer USER_KEY" http://localhost:8080/api/admin/users# {"error":"Insufficient permissions","code":"FORBIDDEN"}# Test admin route with permissioncurl -H "Authorization: Bearer ADMIN_KEY" http://localhost:8080/api/admin/users# {"message":"Admin access granted","users":["user1","user2"]}Related#
Go Quickstart
Get started with Go and Unkey
Go SDK Reference
Complete Go SDK documentation