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

Complete Middleware Implementation

package main

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

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    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")),
    )
}

// UnkeyAuthMiddleware creates an Echo middleware for API key verification
func UnkeyAuthMiddleware() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            authHeader := c.Request().Header.Get("Authorization")
            if authHeader == "" {
                return c.JSON(http.StatusUnauthorized, map[string]string{
                    "error": "Missing Authorization header",
                    "code":  "MISSING_KEY",
                })
            }

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

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

            if err != nil {
                return c.JSON(http.StatusServiceUnavailable, map[string]string{
                    "error":   "Verification service unavailable",
                    "code":    "SERVICE_ERROR",
                    "message": err.Error(),
                })
            }

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

                return c.JSON(http.StatusUnauthorized, map[string]string{
                    "error": "Invalid API key",
                    "code":  code,
                })
            }

            // Store verification result in context
            c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data)
            return next(c)
        }
    }
}

// RequirePermission creates middleware to check specific permissions
func RequirePermission(permission string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            result := c.Get("unkey")
            if result == nil {
                return c.JSON(http.StatusUnauthorized, map[string]string{
                    "error": "Authentication required",
                    "code":  "AUTH_REQUIRED",
                })
            }

            keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData)
            if !ok || keyResult == nil {
                return c.JSON(http.StatusInternalServerError, map[string]any{
                    "error": "Invalid authentication context",
                    "code":  "INTERNAL_ERROR",
                })
            }

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

            return c.JSON(http.StatusForbidden, map[string]any{
                "error":    "Insufficient permissions",
                "code":     "FORBIDDEN",
                "required": permission,
            })
        }
    }
}

// RequireRole creates middleware to check specific roles
func RequireRole(role string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            result := c.Get("unkey")
            if result == nil {
                return c.JSON(http.StatusUnauthorized, map[string]string{
                    "error": "Authentication required",
                    "code":  "AUTH_REQUIRED",
                })
            }

            keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData)
            if !ok || keyResult == nil {
                return c.JSON(http.StatusInternalServerError, map[string]any{
                    "error": "Invalid authentication context",
                    "code":  "INTERNAL_ERROR",
                })
            }

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

            return c.JSON(http.StatusForbidden, map[string]any{
                "error":    "Insufficient role",
                "code":     "FORBIDDEN",
                "required": role,
            })
        }
    }
}

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

// Usage example
func main() {
    e := echo.New()
    
    // Middleware
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    // Public routes
    e.GET("/health", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
    })

    // Protected routes
    api := e.Group("/api")
    api.Use(UnkeyAuthMiddleware())
    {
        api.GET("/data", func(c echo.Context) error {
            result := GetUnkeyResult(c)
            ownerID := ""
            if result.Identity != nil {
                ownerID = result.Identity.ExternalID
            }
            return c.JSON(http.StatusOK, map[string]any{
                "message": "Access granted",
                "key_id":  *result.KeyID,
                "owner":   ownerID,
                "meta":    result.Meta,
            })
        })

        api.GET("/profile", func(c echo.Context) error {
            result := GetUnkeyResult(c)
            return c.JSON(http.StatusOK, map[string]any{
                "key_id":      *result.KeyID,
                "permissions": result.Permissions,
                "roles":       result.Roles,
            })
        })
    }

    // Admin routes
    admin := e.Group("/api/admin")
    admin.Use(UnkeyAuthMiddleware(), RequirePermission("admin:read"))
    {
        admin.GET("/users", func(c echo.Context) error {
            return c.JSON(http.StatusOK, map[string]any{
                "message": "Admin access granted",
                "users":   []string{"user1", "user2"},
            })
        })

        admin.POST("/config", func(c echo.Context) error {
            return c.JSON(http.StatusOK, map[string]string{
                "message": "Config updated",
            })
        }, RequirePermission("admin:write"))
    }

    e.Start(":8080")
}

Custom Configuration

Create configurable middleware:
type AuthConfig struct {
    HeaderName string
    Prefix     string
    Optional   bool
}

func UnkeyAuthWithConfig(config AuthConfig) echo.MiddlewareFunc {
    if config.HeaderName == "" {
        config.HeaderName = "Authorization"
    }
    if config.Prefix == "" {
        config.Prefix = "Bearer "
    }

    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            authHeader := c.Request().Header.Get(config.HeaderName)
            
            if authHeader == "" {
                if config.Optional {
                    return next(c)
                }
                return c.JSON(http.StatusUnauthorized, map[string]string{
                    "error": "Missing API key",
                    "code":  "MISSING_KEY",
                })
            }

            apiKey := strings.TrimPrefix(authHeader, config.Prefix)

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

            if err != nil {
                return c.JSON(http.StatusServiceUnavailable, map[string]string{
                    "error": "Verification failed",
                })
            }

            if !res.V2KeysVerifyKeyResponseBody.Data.Valid {
                return c.JSON(http.StatusUnauthorized, map[string]string{
                    "error": "Invalid API key",
                })
            }

            c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data)
            return next(c)
        }
    }
}

// Usage with custom config
e.GET("/api/public", func(c echo.Context) error {
    result := GetUnkeyResult(c)
    if result != nil {
        return c.JSON(http.StatusOK, map[string]any{
            "message": "Authenticated",
            "key_id":  *result.KeyID,
        })
    }
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Anonymous",
    })
}, UnkeyAuthWithConfig(AuthConfig{Optional: true}))

Group-Level Middleware

Apply middleware to route groups:
// API v1 routes
v1 := e.Group("/api/v1")
v1.Use(UnkeyAuthMiddleware())

// Public subset
public := v1.Group("/public")
public.GET("/status", statusHandler)

// Protected subset
protected := v1.Group("/protected")
protected.Use(RequirePermission("data:read"))
protected.GET("/data", dataHandler)

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