Skip to main content

Documentation Index

Fetch the complete documentation index at: https://unkey.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

This recipe shows how to create robust API key authentication middleware for Go’s standard library HTTP server.

Complete Middleware Implementation

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "os"
    "slices"
    "strings"
    "time"

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

// KeyContext stores Unkey verification result in request context
type KeyContext struct {
    KeyID       string
    OwnerID     string
    Meta        map[string]any
    Permissions []string
    Roles       []string
}

// contextKey is the key type for storing Unkey context
type contextKey string

const unkeyContextKey contextKey = "unkey"

var unkeyClient *unkey.Unkey

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

// AuthMiddleware creates a middleware that verifies API keys
func AuthMiddleware(opts ...AuthOption) func(http.Handler) http.Handler {
    options := &authOptions{
        headerName: "Authorization",
        prefix:     "Bearer ",
        required:   true,
    }

    for _, opt := range opts {
        opt(options)
    }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/json")

            // Extract API key
            authHeader := r.Header.Get(options.headerName)
            if authHeader == "" {
                if options.required {
                    w.WriteHeader(http.StatusUnauthorized)
                    json.NewEncoder(w).Encode(map[string]string{
                        "error": "Missing API key",
                        "code":  "MISSING_KEY",
                    })
                    return
                }
                // Auth not required, continue without verification
                next.ServeHTTP(w, r)
                return
            }

            apiKey := strings.TrimPrefix(authHeader, options.prefix)

            // Verify with Unkey
            ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
            defer cancel()

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

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

            result := res.V2KeysVerifyKeyResponseBody.Data

            if !result.Valid {
                code := string(result.Code)

                w.WriteHeader(http.StatusUnauthorized)
                json.NewEncoder(w).Encode(map[string]any{
                    "error": "Invalid API key",
                    "code":  code,
                })
                return
            }

            // Build context
            keyCtx := &KeyContext{
                KeyID:       *result.KeyID,
                Meta:        result.Meta,
                Permissions: result.Permissions,
                Roles:       result.Roles,
            }

            if result.Identity != nil {
                keyCtx.OwnerID = result.Identity.ExternalID
            }

            // Store in request context
            ctx = context.WithValue(r.Context(), unkeyContextKey, keyCtx)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// AuthOption configures the auth middleware
type authOptions struct {
    headerName string
    prefix     string
    required   bool
}

type AuthOption func(*authOptions)

// WithHeaderName sets a custom header name for the API key
func WithHeaderName(name string) AuthOption {
    return func(o *authOptions) {
        o.headerName = name
    }
}

// WithPrefix sets a custom prefix for the API key
func WithPrefix(prefix string) AuthOption {
    return func(o *authOptions) {
        o.prefix = prefix
    }
}

// WithOptional makes authentication optional
func WithOptional() AuthOption {
    return func(o *authOptions) {
        o.required = false
    }
}

// GetKeyContext retrieves the Unkey context from request
func GetKeyContext(r *http.Request) (*KeyContext, bool) {
    ctx, ok := r.Context().Value(unkeyContextKey).(*KeyContext)
    return ctx, ok
}

// RequirePermission middleware checks if the key has a specific permission
func RequirePermission(permission string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            keyCtx, ok := GetKeyContext(r)
            if !ok {
                w.WriteHeader(http.StatusUnauthorized)
                json.NewEncoder(w).Encode(map[string]string{
                    "error": "Authentication required",
                    "code":  "AUTH_REQUIRED",
                })
                return
            }

            if slices.Contains(keyCtx.Permissions, permission) {
                next.ServeHTTP(w, r)
                return
            }

            w.WriteHeader(http.StatusForbidden)
            json.NewEncoder(w).Encode(map[string]string{
                "error": "Insufficient permissions",
                "code":  "FORBIDDEN",
                "required": permission,
            })
        })
    }
}

// Usage example
func main() {
    mux := http.NewServeMux()

    // Public route
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    // Protected routes
    mux.Handle("/api/protected", AuthMiddleware()(http.HandlerFunc(protectedHandler)))

    // Protected with permission check
    // Compose middleware: AuthMiddleware wraps RequirePermission which wraps the handler
    mux.Handle("/api/admin", AuthMiddleware()(RequirePermission("admin:read")(http.HandlerFunc(adminHandler))))

    // Optional auth
    mux.Handle("/api/public", AuthMiddleware(WithOptional())(http.HandlerFunc(optionalAuthHandler)))

    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
    }

    server.ListenAndServe()
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
    keyCtx, _ := GetKeyContext(r)

    json.NewEncoder(w).Encode(map[string]any{
        "message": "Access granted",
        "key_id":  keyCtx.KeyID,
        "owner":   keyCtx.OwnerID,
    })
}

func adminHandler(w http.ResponseWriter, r *http.Request) {
    keyCtx, _ := GetKeyContext(r)

    json.NewEncoder(w).Encode(map[string]any{
        "message": "Admin access granted",
        "key_id":  keyCtx.KeyID,
    })
}

func optionalAuthHandler(w http.ResponseWriter, r *http.Request) {
    keyCtx, ok := GetKeyContext(r)

    if ok {
        json.NewEncoder(w).Encode(map[string]any{
            "message": "Authenticated access",
            "key_id":  keyCtx.KeyID,
        })
    } else {
        json.NewEncoder(w).Encode(map[string]any{
            "message": "Anonymous access",
        })
    }
}

Key Features

  • Context propagation - Key info stored in request context
  • Permission checking - Middleware to check specific permissions
  • Optional auth - Support for optional authentication
  • Custom headers - Configurable header names and prefixes
  • Timeout handling - Request timeouts for Unkey API calls
  • Error responses - Structured JSON error responses

Testing

# Start server
go run main.go

# Test protected route without key
curl http://localhost:8080/api/protected
# {"error":"Missing API key","code":"MISSING_KEY"}

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

# Test public route (no auth required)
curl http://localhost:8080/api/public
# {"message":"Anonymous access"}

# Test public route with auth
curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/public
# {"message":"Authenticated access","key_id":"key_..."}

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

# Test admin route with permission
curl -H "Authorization: Bearer ADMIN_API_KEY" http://localhost:8080/api/admin
# {"message":"Admin access granted","key_id":"key_..."}

Go SDK Reference

Complete Go SDK documentation
Last modified on April 7, 2026