This recipe shows how to create robust API key authentication middleware for Go's standard library HTTP server.
Complete Middleware Implementation#
package mainimport ( "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 contexttype KeyContext struct { KeyID string IdentityExternalID string Meta map[string]any Permissions []string Roles []string}// contextKey is the key type for storing Unkey contexttype contextKey stringconst unkeyContextKey contextKey = "unkey"var unkeyClient *unkey.Unkeyfunc init() { unkeyClient = unkey.New( unkey.WithSecurity(os.Getenv("UNKEY_ROOT_KEY")), )}// AuthMiddleware creates a middleware that verifies API keysfunc 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.IdentityExternalID = result.Identity.ExternalID } // Store in request context ctx = context.WithValue(r.Context(), unkeyContextKey, keyCtx) next.ServeHTTP(w, r.WithContext(ctx)) }) }}// AuthOption configures the auth middlewaretype authOptions struct { headerName string prefix string required bool}type AuthOption func(*authOptions)// WithHeaderName sets a custom header name for the API keyfunc WithHeaderName(name string) AuthOption { return func(o *authOptions) { o.headerName = name }}// WithPrefix sets a custom prefix for the API keyfunc WithPrefix(prefix string) AuthOption { return func(o *authOptions) { o.prefix = prefix }}// WithOptional makes authentication optionalfunc WithOptional() AuthOption { return func(o *authOptions) { o.required = false }}// GetKeyContext retrieves the Unkey context from requestfunc 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 permissionfunc 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 examplefunc 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, "identity_external_id": keyCtx.IdentityExternalID, })}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 servergo run main.go# Test protected route without keycurl http://localhost:8080/api/protected# {"error":"Missing API key","code":"MISSING_KEY"}# Test protected route with valid keycurl -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 authcurl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8080/api/public# {"message":"Authenticated access","key_id":"key_..."}# Test admin route without permissioncurl -H "Authorization: Bearer USER_API_KEY" http://localhost:8080/api/admin# {"error":"Insufficient permissions","code":"FORBIDDEN","required":"admin:read"}# Test admin route with permissioncurl -H "Authorization: Bearer ADMIN_API_KEY" http://localhost:8080/api/admin# {"message":"Admin access granted","key_id":"key_..."}Related#
Go SDK Reference
Complete Go SDK documentation