Overview
This guide covers everything you need to protect your Go API with Unkey, including middleware for the standard library, Gin framework, and Echo framework. What you’ll learn:- Quick setup with the Unkey Go SDK
- Middleware for
net/http(standard library) - Gin framework integration
- Echo framework integration
- Permission-based access control
- Production-ready patterns
Prerequisites
- Go 1.21 or higher
- Unkey account (free)
- API created in your Unkey dashboard
Quick Start
Set up credentials
Get a root key from Settings → Root Keys and set it as an environment variable:
export UNKEY_ROOT_KEY="unkey_..."
Create middleware
Here’s how to verify API keys with standard library
net/http:main.go
package main
import (
"context"
"net/http"
"os"
"strings"
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")),
)
}
// AuthMiddleware verifies API keys on incoming requests
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract API key from header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, `{"error": "Missing Authorization header"}`, http.StatusUnauthorized)
return
}
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
// Verify with Unkey
res, err := unkeyClient.Keys.VerifyKey(r.Context(), components.V2KeysVerifyKeyRequestBody{
Key: apiKey,
})
if err != nil {
http.Error(w, `{"error": "Verification service unavailable"}`, http.StatusServiceUnavailable)
return
}
result := res.V2KeysVerifyKeyResponseBody.Data
if !result.Valid {
http.Error(w, `{"error": "Invalid API key"}`, http.StatusUnauthorized)
return
}
// Add key info to context for handlers
ctx := context.WithValue(r.Context(), "keyId", *result.KeyID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
mux := http.NewServeMux()
// Protected route
mux.HandleFunc("/api/protected", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "Access granted!"}`))
})
// Wrap with auth middleware
http.ListenAndServe(":8080", AuthMiddleware(mux))
}
Standard Library (net/http)
Production-Ready Middleware
Here’s a complete middleware implementation with context, options, and error handling:middleware/auth.go
package middleware
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
}
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")
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
}
next.ServeHTTP(w, r)
return
}
apiKey := strings.TrimPrefix(authHeader, options.prefix)
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
}
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)
func WithHeaderName(name string) AuthOption {
return func(o *authOptions) {
o.headerName = name
}
}
func WithPrefix(prefix string) AuthOption {
return func(o *authOptions) {
o.prefix = prefix
}
}
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,
})
})
}
}
main.go
package main
import (
"encoding/json"
"net/http"
"time"
"yourapp/middleware"
)
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", middleware.AuthMiddleware()(http.HandlerFunc(protectedHandler)))
// Protected with permission check
mux.Handle("/api/admin", middleware.AuthMiddleware()(middleware.RequirePermission("admin:read")(http.HandlerFunc(adminHandler))))
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, _ := middleware.GetKeyContext(r)
json.NewEncoder(w).Encode(map[string]any{
"message": "Access granted",
"key_id": keyCtx.KeyID,
})
}
func adminHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{
"message": "Admin access granted",
})
}
Gin Framework
For the Gin web framework:go get github.com/gin-gonic/gin
main.go
package main
import (
"net/http"
"os"
"slices"
"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")),
)
}
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 ")
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
c.Set("unkey", &res.V2KeysVerifyKeyResponseBody.Data)
c.Next()
}
}
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",
})
return
}
keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData)
if !ok || keyResult == nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "Invalid authentication context",
})
return
}
if !slices.Contains(keyResult.Permissions, permission) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Insufficient permissions",
"required": permission,
})
return
}
c.Next()
}
}
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, _ := c.Get("unkey")
keyResult := result.(*components.V2KeysVerifyKeyResponseData)
c.JSON(http.StatusOK, gin.H{
"message": "Access granted",
"key_id": *keyResult.KeyID,
})
})
}
// 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"},
})
})
}
r.Run(":8080")
}
Echo Framework
For the Echo web framework:go get github.com/labstack/echo/v4
main.go
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")),
)
}
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",
})
}
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
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",
})
}
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)
}
}
}
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",
})
}
keyResult, ok := result.(*components.V2KeysVerifyKeyResponseData)
if !ok || keyResult == nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Invalid authentication context",
})
}
if !slices.Contains(keyResult.Permissions, permission) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
"required": permission,
})
}
return next(c)
}
}
}
func main() {
e := echo.New()
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 := c.Get("unkey").(*components.V2KeysVerifyKeyResponseData)
return c.JSON(http.StatusOK, map[string]any{
"message": "Access granted",
"key_id": *result.KeyID,
})
})
}
// 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"},
})
})
}
e.Start(":8080")
}
Next Steps
Add rate limiting
Protect your endpoints from abuse
Go SDK Reference
Complete Go SDK documentation
Authorization
Add roles and permissions
Cookbook
More Go recipes and examples