API Key Authentication

Learn how to implement API Key Authentication in your Goa API

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.

How API Key Auth Works

API Keys can be transmitted in several ways:

  1. As a header (most common)
  2. As a query parameter
  3. In the request body

The most secure method is using headers, typically with a name like X-API-Key or Authorization.

Implementing API Key Auth in Goa

1. Define the Security Scheme

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
})

2. Apply the Security Scheme

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)
})

3. Define the Payload

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)
    })
})

4. Implement the Security Handler

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
}

Best Practices for API Key Auth

1. Key Generation

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)
}

2. Key Storage

Store API keys securely:

  • Hash keys before storing them
  • Use secure key-value stores or databases
  • Implement key rotation mechanisms

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
);

4. Key Metadata

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
}

Example Implementation

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")
})

Generated Code

Goa generates several components for API Key auth:

  1. Security Types

    • Types for API key
    • Error types for authentication failures
  2. Middleware

    • Extracts API key from request
    • Calls your security handler
    • Handles authentication errors
  3. OpenAPI Documentation

    • Documents security requirements
    • Shows API key location (header/query)
    • Documents error responses

Common Issues and Solutions

1. Key Not Being Sent

If the API key isn’t being sent correctly, check:

  • Header name matches exactly
  • Key format is correct
  • Client is actually sending the key

2. Performance Considerations

For high-traffic APIs:

  • Cache API key validation results
  • Use fast key-value stores
  • Implement key prefixing for quick invalidation

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
}

Next Steps