API Key authentication is a simple and popular way to secure APIs. It involves distributing unique keys to clients who then include these keys in their requests. This method is particularly useful for public APIs where you want to track usage, implement rate limiting, or provide different access levels to different clients.
API Keys can be transmitted in several ways:
The most secure method is using headers, typically with a name like X-API-Key
or Authorization
.
First, define your API Key security scheme in your design package:
package design
import (
. "goa.design/goa/v3/dsl"
)
// APIKeyAuth defines our security scheme
var APIKeyAuth = APIKeySecurity("api_key", func() {
Description("API key security")
Header("X-API-Key") // Specify header name
})
You can also use query parameters instead of headers:
var APIKeyAuth = APIKeySecurity("api_key", func() {
Description("API key security")
Query("api_key") // Specify query parameter name
})
Like other security schemes, API Key auth can be applied at different levels:
// API level - applies to all services and methods
var _ = API("secure_api", func() {
Security(APIKeyAuth)
})
// Service level - applies to all methods in the service
var _ = Service("secure_service", func() {
Security(APIKeyAuth)
})
// Method level - applies only to this method
Method("secure_method", func() {
Security(APIKeyAuth)
})
For methods that use API Key auth, include the key in the payload:
Method("getData", func() {
Security(APIKeyAuth)
Payload(func() {
APIKey("api_key", "key", String, func() {
Description("API key for authentication")
Example("abcdef123456")
})
Required("key")
// Additional payload fields
Field(1, "query", String, "Search query")
})
Result(ArrayOf(String))
Error("unauthorized")
HTTP(func() {
GET("/data")
// Map the key to the header
Header("key:X-API-Key")
Response("unauthorized", StatusUnauthorized)
})
})
When Goa generates the code, you’ll need to implement a security handler:
// SecurityAPIKeyFunc implements the authorization logic for API Key auth
func (s *service) APIKeyAuth(ctx context.Context, key string) (context.Context, error) {
// Implement your key validation logic here
valid, err := s.validateAPIKey(key)
if err != nil {
return ctx, err
}
if !valid {
return ctx, genservice.MakeUnauthorized(fmt.Errorf("invalid API key"))
}
// You can add key-specific data to the context
ctx = context.WithValue(ctx, "api_key_id", key)
return ctx, nil
}
func (s *service) validateAPIKey(key string) (bool, error) {
// Implementation of key validation
// This could check against a database, cache, etc.
return key == "valid-key", nil
}
Generate strong, random API keys:
func GenerateAPIKey() string {
// Generate 32 random bytes
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
panic(err)
}
// Encode as base64
return base64.URLEncoding.EncodeToString(bytes)
}
Store API keys securely:
Example key storage schema:
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
key_hash VARCHAR(64) NOT NULL,
client_id UUID NOT NULL,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP,
last_used_at TIMESTAMP,
is_active BOOLEAN DEFAULT true
);
Associate metadata with API keys for better control:
type APIKeyMetadata struct {
ClientID string
Plan string // e.g., "free", "premium"
Permissions []string // e.g., ["read", "write"]
ExpiresAt time.Time
}
func (s *service) APIKeyAuth(ctx context.Context, key string) (context.Context, error) {
metadata, err := s.getAPIKeyMetadata(key)
if err != nil {
return ctx, err
}
// Add metadata to context
ctx = context.WithValue(ctx, "api_key_metadata", metadata)
return ctx, nil
}
Here’s a complete example showing how to implement API Key auth in a Goa service:
package design
import (
. "goa.design/goa/v3/dsl"
)
var APIKeyAuth = APIKeySecurity("api_key", func() {
Description("Authenticate using an API key")
Header("X-API-Key")
})
var _ = API("weather_api", func() {
Title("Weather API")
Description("Weather forecast API with API key authentication")
// Apply API key auth by default
Security(APIKeyAuth)
})
var _ = Service("weather", func() {
Description("Weather forecast service")
Method("forecast", func() {
Description("Get weather forecast")
Payload(func() {
// API key will be automatically included
Field(1, "location", String, "Location to get forecast for")
Field(2, "days", Int, "Number of days to forecast")
Required("location")
})
Result(func() {
Field(1, "location", String, "Location")
Field(2, "forecast", ArrayOf(WeatherDay))
})
HTTP(func() {
GET("/forecast/{location}")
Param("days")
Response(StatusOK)
Response(StatusUnauthorized, func() {
Description("Invalid or missing API key")
})
Response(StatusTooManyRequests, func() {
Description("Rate limit exceeded")
})
})
})
// Public endpoint example
Method("health", func() {
Description("Health check endpoint")
NoSecurity()
Result(String)
HTTP(func() {
GET("/health")
})
})
})
// WeatherDay defines the weather forecast for a single day
var WeatherDay = Type("WeatherDay", func() {
Field(1, "date", String, "Forecast date")
Field(2, "temperature", Float64, "Temperature in Celsius")
Field(3, "conditions", String, "Weather conditions")
Required("date", "temperature", "conditions")
})
Goa generates several components for API Key auth:
Security Types
Middleware
OpenAPI Documentation
If the API key isn’t being sent correctly, check:
For high-traffic APIs:
Example caching implementation:
func (s *service) APIKeyAuth(ctx context.Context, key string) (context.Context, error) {
// Check cache first
if metadata, found := s.cache.Get(key); found {
return context.WithValue(ctx, "api_key_metadata", metadata), nil
}
// Validate key and get metadata
metadata, err := s.validateAPIKey(key)
if err != nil {
return ctx, err
}
// Cache the result
s.cache.Set(key, metadata, time.Minute*5)
return context.WithValue(ctx, "api_key_metadata", metadata), nil
}