Building a secure API involves more than just adding authentication. You need to think about security at every level of your application, from how you handle user input to how you protect your server from attacks. This guide will walk you through essential security practices for your Goa API, with practical examples you can implement today.
Security isn’t about having a single strong lock - it’s about having multiple layers of protection. If one layer fails, the others are still there to protect your application. Here’s how to implement multiple security layers in your Goa service:
// First layer use HTTPS
var _ = Service("secure_service", func() {
Security(JWTAuth, func() { // Second layer: Require valid authentication
Scope("api:write") // Third layer: Check specific permissions
})
// Fourth layer: Validate all input
Method("secureEndpoint", func() {
Payload(func() {
Field(1, "data", String)
MaxLength("data", 1000) // Prevent large payloads
})
})
})
This code demonstrates how to layer multiple security controls. Think of it like a medieval castle - you have the moat (HTTPS), the outer wall (authentication), the inner wall (authorization), and finally, the careful inspection of all visitors (input validation).
For rate limiting, you’ll want to implement it using middleware on your Goa HTTP server. Here’s how to add rate limiting to your service:
package main
import (
"context"
"net/http"
"time"
"golang.org/x/time/rate"
goahttp "goa.design/goa/v3/http"
"goa.design/goa/v3/middleware"
)
// RateLimiter creates middleware that limits request rate
func RateLimiter(limit rate.Limit, burst int) middleware.Middleware {
limiter := rate.NewLimiter(limit, burst)
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
h.ServeHTTP(w, r)
})
}
}
func main() {
// ... logger, instrumentation setup ...
// Create service & endpoints
svc := NewService()
endpoints := gen.NewEndpoints(svc)
mux := goahttp.NewMuxer()
// Create server
server := gen.NewServer(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
// Mount generated handlers
gen.Mount(mux, server)
// Add middleware to the server handler chain
var handler http.Handler = mux
handler = RateLimiter(rate.Every(time.Second/100), 10)(handler) // 100 req/sec
handler = log.HTTP(ctx)(handler) // Add logging
// Create and start HTTP server
srv := &http.Server{
Addr: ":8080",
Handler: handler,
}
// ... graceful shutdown code ...
}
For per-endpoint rate limiting, you can apply the rate limiter directly to specific endpoints:
// RateLimitEndpoint wraps an endpoint with rate limiting
func RateLimitEndpoint(limit rate.Limit, burst int) func(goa.Endpoint) goa.Endpoint {
limiter := rate.NewLimiter(limit, burst)
return func(endpoint goa.Endpoint) goa.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if !limiter.Allow() {
return nil, fmt.Errorf("rate limit exceeded")
}
return endpoint(ctx, req)
}
}
}
func main() {
// ... service setup code ...
// Create endpoints
endpoints := &gen.Endpoints{
Forecast: RateLimitEndpoint(rate.Every(time.Second), 10)(
gen.NewForecastEndpoint(svc),
),
TestAll: gen.NewTestAllEndpoint(svc), // No rate limit
TestSmoke: RateLimitEndpoint(rate.Every(time.Minute), 5)(
gen.NewTestSmokeEndpoint(svc),
),
}
// ... rest of server setup ...
}
This approach:
One of the most important security principles is to start with secure defaults. It’s much safer to start with everything locked down and then selectively open access, rather than starting open and trying to lock things down later. Here’s how to set secure defaults in your Goa API:
var _ = API("secure_api", func() {
// Require authentication by default
Security(JWTAuth)
})
These settings ensure that every endpoint in your API requires authentication by default. For transport security (HTTPS), you’ll configure this at the server level in your implementation:
func main() {
// ... service and endpoint setup ...
// Create TLS configuration
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
// Create HTTPS server with secure configuration
srv := &http.Server{
Addr: ":443",
Handler: handler,
TLSConfig: tlsConfig,
// Set timeouts to prevent slow-loris attacks
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Start server with TLS
log.Printf("HTTPS server listening on %s", srv.Addr)
if err := srv.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
log.Fatalf("failed to start HTTPS server: %v", err)
}
}
This implementation:
You can also combine this with other security middleware like rate limiting:
When it comes to permissions, less is more. Every user and service should have exactly the permissions they need to do their job - no more, no less. This limits the potential damage if any single account is compromised. Here’s how to implement fine-grained permissions in your API:
var _ = Service("user_service", func() {
// Regular users can read their own profile
Method("getProfile", func() {
Security(OAuth2Auth, func() {
Scope("profile:read")
})
// Implementation ensures users can only read their own profile
Payload(func() {
UserID("id", String, "Profile to read")
})
})
// Only users with write permission can update profiles
Method("updateProfile", func() {
Security(OAuth2Auth, func() {
Scope("profile:write")
})
})
// Administrative operations require special privileges
Method("deleteUser", func() {
Security(OAuth2Auth, func() {
Scope("admin")
})
})
})
This example shows how to create a hierarchy of permissions. Regular users can read their own data, users with elevated privileges can make changes, and only administrators can perform dangerous operations like deletions.
Proper authentication is your API’s first line of defense. Let’s look at how to implement secure authentication practices.
Tokens are like digital keys to your API. Just like physical keys, they need to be created securely, checked carefully, and managed throughout their lifecycle. Here’s how to implement secure token handling:
// Generate a new token with appropriate security measures
func GenerateToken(user *User) (string, error) {
now := time.Now()
claims := &Claims{
StandardClaims: jwt.StandardClaims{
// Token is valid starting now
IssuedAt: now.Unix(),
// Token expires in 24 hours
ExpiresAt: now.Add(time.Hour * 24).Unix(),
// Identify who issued the token
Issuer: "your-api",
// Identify who the token belongs to
Subject: user.ID,
},
// Include user's permissions
Scopes: user.Permissions,
}
// Use a secure signing method (ECDSA is more secure than HMAC)
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
return token.SignedString(privateKey)
}
// Validate incoming tokens thoroughly
func ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{},
func(token *jwt.Token) (interface{}, error) {
// Always verify the signing method
if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
// Perform additional validation
if err := validateClaims(claims); err != nil {
return nil, err
}
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
Implement secure password handling:
// Hash passwords using strong algorithms
func HashPassword(password string) (string, error) {
// Use bcrypt with appropriate cost
hash, err := bcrypt.GenerateFromPassword(
[]byte(password),
bcrypt.DefaultCost,
)
if err != nil {
return "", err
}
return string(hash), nil
}
// Verify passwords
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword(
[]byte(hashedPassword),
[]byte(password),
)
}
Implement secure API key handling:
// Generate secure API keys
func GenerateAPIKey() string {
// Use crypto/rand for secure random generation
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString(bytes)
}
// Store API keys securely
func StoreAPIKey(key string) error {
// Hash the key before storage
hashedKey := sha256.Sum256([]byte(key))
// Store in database
return db.StoreKey(hex.EncodeToString(hashedKey[:]))
}
Implement RBAC using scopes:
var _ = Service("admin", func() {
// Define roles and permissions
Security(OAuth2Auth, func() {
Scope("admin:read", "Read admin resources")
Scope("admin:write", "Modify admin resources")
Scope("admin:delete", "Delete admin resources")
})
Method("getUsers", func() {
Security(OAuth2Auth, func() {
Scope("admin:read")
})
})
Method("createUser", func() {
Security(OAuth2Auth, func() {
Scope("admin:write")
})
})
Method("deleteUser", func() {
Security(OAuth2Auth, func() {
Scope("admin:delete")
})
})
})
Implement resource-level authorization:
func (s *service) authorizeResource(ctx context.Context,
resourceID string) error {
// Get user from context
user := auth.UserFromContext(ctx)
// Get resource
resource, err := s.db.GetResource(resourceID)
if err != nil {
return err
}
// Check ownership or permissions
if !canAccess(user, resource) {
return fmt.Errorf("unauthorized access to resource")
}
return nil
}
Define comprehensive validation rules:
var _ = Type("UserInput", func() {
Field(1, "username", String, func() {
Pattern("^[a-zA-Z0-9_]{3,30}$")
Example("john_doe")
})
Field(2, "email", String, func() {
Format(FormatEmail)
Example("[email protected]")
})
Field(3, "age", Int, func() {
Minimum(18)
Maximum(150)
Example(25)
})
Field(4, "website", String, func() {
Format(FormatURI)
Example("https://example.com")
})
Required("username", "email", "age")
})
Implement content security measures:
var _ = Service("content", func() {
HTTP(func() {
Response(func() {
// Set Content Security Policy
Header("Content-Security-Policy", String,
"default-src 'self'")
// Prevent MIME type sniffing
Header("X-Content-Type-Options", String, "nosniff")
// Control frame embedding
Header("X-Frame-Options", String, "DENY")
})
})
})
Implement rate limiting at multiple levels:
var _ = Service("api", func() {
// Global rate limit
Meta("ratelimit:limit", "1000")
Meta("ratelimit:window", "1h")
// Method-specific rate limits
Method("expensive", func() {
Meta("ratelimit:limit", "10")
Meta("ratelimit:window", "1m")
})
})
Implement DOS protection measures:
var _ = Service("api", func() {
// Limit payload size
MaxLength("request_body", 1024*1024) // 1MB limit
// Timeout for long operations
Meta("timeout", "30s")
// Pagination limits
Method("list", func() {
Payload(func() {
Field(1, "page", Int, func() {
Minimum(1)
})
Field(2, "per_page", Int, func() {
Minimum(1)
Maximum(100)
})
})
})
})
Implement secure error responses:
var _ = Service("api", func() {
Error("unauthorized", func() {
Description("Authentication failed")
// Don't expose internal details
Field(1, "message", String, "Authentication required")
})
Error("validation_error", func() {
Description("Invalid input")
Field(1, "fields", ArrayOf(String), "Invalid fields")
})
Method("secure", func() {
Error("unauthorized")
Error("validation_error")
HTTP(func() {
Response("unauthorized", StatusUnauthorized)
Response("validation_error", StatusBadRequest)
})
})
})
Implement secure logging practices:
func (s *service) logSecurityEvent(ctx context.Context,
eventType string, details map[string]interface{}) {
// Add security context
details["ip_address"] = getClientIP(ctx)
details["user_id"] = getUserID(ctx)
details["timestamp"] = time.Now().UTC()
// Never log sensitive data
delete(details, "password")
delete(details, "token")
// Log with appropriate level
s.logger.WithFields(details).Info(eventType)
}
Enforce HTTPS usage:
var _ = API("secure_api", func() {
// Require HTTPS
Meta("transport", "https")
HTTP(func() {
// Redirect HTTP to HTTPS
Meta("redirect_http", "true")
// Set HSTS header
Response(func() {
Header("Strict-Transport-Security",
String,
"max-age=31536000; includeSubDomains")
})
})
})
Implement proper certificate handling:
func setupTLS() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
}
Write security-focused tests:
func TestSecurityHandling(t *testing.T) {
tests := []struct {
name string
token string
expectedCode int
expectedBody string
}{
{
name: "valid_token",
token: generateValidToken(),
expectedCode: http.StatusOK,
},
{
name: "expired_token",
token: generateExpiredToken(),
expectedCode: http.StatusUnauthorized,
},
{
name: "invalid_signature",
token: generateTokenWithInvalidSignature(),
expectedCode: http.StatusUnauthorized,
},
{
name: "missing_token",
token: "",
expectedCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}
Implement security scanning in your pipeline:
# Example GitHub Actions workflow
name: Security Scan
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: ./...
- name: Run Nancy for Dependency Scanning
uses: sonatype-nexus-community/nancy-github-action@main
- name: Run OWASP ZAP Scan
uses: zaproxy/[email protected]
Implement security monitoring:
func monitorSecurityEvents(ctx context.Context) {
// Monitor authentication failures
go monitorAuthFailures(ctx)
// Monitor rate limit breaches
go monitorRateLimits(ctx)
// Monitor suspicious patterns
go monitorSuspiciousActivity(ctx)
}
func monitorAuthFailures(ctx context.Context) {
threshold := 5
window := time.Minute * 5
for {
select {
case <-ctx.Done():
return
default:
failures := getRecentAuthFailures(window)
if failures > threshold {
alertSecurityTeam("High authentication failure rate detected")
}
time.Sleep(time.Minute)
}
}
}
Prepare incident response handlers:
func handleSecurityIncident(incident *SecurityIncident) {
// Log incident details
logSecurityIncident(incident)
// Alert security team
alertSecurityTeam(incident)
// Take immediate action
switch incident.Type {
case "brute_force_attempt":
blockIP(incident.SourceIP)
case "api_key_compromise":
revokeAPIKey(incident.APIKey)
case "unauthorized_access":
terminateUserSessions(incident.UserID)
}
// Create incident report
createIncidentReport(incident)
}