When securing APIs, it’s important to understand two distinct concepts:
Goa provides DSL constructs to define both authentication and authorization requirements for your services.
JWT is an open standard (RFC 7519) that defines a compact way to securely transmit information between parties as a JSON object. JWTs are often used for both authentication and authorization:
var JWTAuth = JWTSecurity("jwt", func() {
Description("JWT-based authentication and authorization")
// Scopes define permissions that can be checked against JWT claims
Scope("api:read", "Read-only access")
Scope("api:write", "Read and write access")
})
Scopes are named permissions that represent what actions a client is allowed to perform. When using JWTs:
API keys are simple string tokens that clients include with their requests. While commonly called “API Key Authentication”, they are more accurately described as an authorization mechanism:
var APIKeyAuth = APIKeySecurity("api_key", func() {
Description("API key-based request authorization")
})
Common uses for API keys:
Basic Authentication is a simple authentication scheme built into the HTTP protocol:
var BasicAuth = BasicAuthSecurity("basic", func() {
Description("Username/password authentication")
// Scopes here define permissions that can be granted after successful authentication
Scope("api:read", "Read-only access")
})
OAuth2 is a comprehensive authorization framework that supports multiple flows for different types of applications. It separates:
var OAuth2Auth = OAuth2Security("oauth2", func() {
// Define the OAuth2 flow endpoints
AuthorizationCodeFlow(
"http://auth.example.com/authorize", // Where to request authorization
"http://auth.example.com/token", // Where to exchange code for token
"http://auth.example.com/refresh", // Where to refresh expired tokens
)
// Define available permissions
Scope("api:read", "Read-only access")
Scope("api:write", "Read and write access")
})
Security schemes can be applied at different levels:
Secure individual methods with one or more schemes:
Method("secure_endpoint", func() {
Security(JWTAuth, func() {
Scope("api:read")
})
Payload(func() {
TokenField(1, "token", String)
Required("token")
})
HTTP(func() {
GET("/secure")
Response(StatusOK)
})
})
Combine multiple security schemes for enhanced security:
Method("doubly_secure", func() {
Security(JWTAuth, APIKeyAuth, func() {
Scope("api:write")
})
Payload(func() {
TokenField(1, "token", String)
APIKeyField(2, "api_key", "key", String)
Required("token", "key")
})
HTTP(func() {
POST("/secure")
Param("key:k") // API key in query parameter
Response(StatusOK)
})
})
Configure how security credentials are transmitted over HTTP:
Method("secure_endpoint", func() {
Security(JWTAuth)
Payload(func() {
TokenField(1, "token", String)
Required("token")
})
HTTP(func() {
GET("/secure")
Header("token:Authorization") // JWT in Authorization header
Response(StatusOK)
Response("unauthorized", StatusUnauthorized)
})
})
Configure security for gRPC transport:
Method("secure_endpoint", func() {
Security(JWTAuth, APIKeyAuth)
Payload(func() {
TokenField(1, "token", String)
APIKeyField(2, "api_key", "key", String)
Required("token", "key")
})
GRPC(func() {
Metadata(func() {
Attribute("token:authorization") // JWT in metadata
Attribute("api_key:x-api-key") // API key in metadata
})
Response(CodeOK)
Response("unauthorized", CodeUnauthenticated)
})
})
Define security-related errors consistently:
Service("secure_service", func() {
Error("unauthorized", String, "Invalid credentials")
Error("forbidden", String, "Invalid scopes")
HTTP(func() {
Response("unauthorized", StatusUnauthorized)
Response("forbidden", StatusForbidden)
})
GRPC(func() {
Response("unauthorized", CodeUnauthenticated)
Response("forbidden", CodePermissionDenied)
})
})
Authentication Design
Authorization Design
General Tips
When you define security schemes in your design, Goa generates an Auther
interface specific to your design that your service must implement. This
interface defines methods for each security scheme you’ve specified:
// Auther defines the security requirements for the service.
type Auther interface {
// BasicAuth implements the authorization logic for basic auth.
BasicAuth(context.Context, string, string, *security.BasicScheme) (context.Context, error)
// JWTAuth implements the authorization logic for JWT tokens.
JWTAuth(context.Context, string, *security.JWTScheme) (context.Context, error)
// APIKeyAuth implements the authorization logic for API keys.
APIKeyAuth(context.Context, string, *security.APIKeyScheme) (context.Context, error)
// OAuth2Auth implements the authorization logic for OAuth2.
OAuth2Auth(context.Context, string, *security.OAuth2Scheme) (context.Context, error)
}
Your service must implement these methods to handle the authentication/authorization logic. Here’s how to implement each:
// BasicAuth implements the authorization logic for the "basic" security scheme.
func (s *svc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) {
if user != "goa" || pass != "rocks" {
return ctx, ErrUnauthorized
}
// Store auth info in context for later use
ctx = contextWithAuthInfo(ctx, authInfo{
user: user,
})
return ctx, nil
}
// JWTAuth implements the authorization logic for the "jwt" security scheme.
func (s *svc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) {
claims := make(jwt.MapClaims)
// Parse and validate JWT token
_, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) {
return Key, nil
})
if err != nil {
return ctx, ErrInvalidToken
}
// Validate required scopes
if claims["scopes"] == nil {
return ctx, ErrInvalidTokenScopes
}
scopes, ok := claims["scopes"].([]any)
if !ok {
return ctx, ErrInvalidTokenScopes
}
scopesInToken := make([]string, len(scopes))
for _, scp := range scopes {
scopesInToken = append(scopesInToken, scp.(string))
}
if err := scheme.Validate(scopesInToken); err != nil {
return ctx, securedservice.InvalidScopes(err.Error())
}
// Store claims in context
ctx = contextWithAuthInfo(ctx, authInfo{
claims: claims,
})
return ctx, nil
}
// APIKeyAuth implements the authorization logic for service "secured_service"
// for the "api_key" security scheme.
func (s *securedServicesrvc) APIKeyAuth(ctx context.Context, key string, scheme *security.APIKeyScheme) (context.Context, error) {
if key != "my_awesome_api_key" {
return ctx, ErrUnauthorized
}
ctx = contextWithAuthInfo(ctx, authInfo{
key: key,
})
return ctx, nil
}
When implementing a sign-in endpoint that issues tokens:
// Signin creates a valid JWT token for authentication
func (s *svc) Signin(ctx context.Context, p *gensvc.SigninPayload) (*gensvc.Creds, error) {
// Create JWT token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
"iat": time.Now().Unix(),
"scopes": []string{"api:read", "api:write"},
})
// Sign the token
t, err := token.SignedString(Key)
if err != nil {
return nil, err
}
return &gensvc.Creds{
JWT: t,
OauthToken: t,
APIKey: "my_awesome_api_key",
}, nil
}
When you implement security schemes in your Goa service, here’s how the authentication and authorization flow works:
For example, with multiple schemes:
// Generated endpoint wrapper
func NewDoublySecureEndpoint(s Service, authJWTFn security.AuthJWTFunc, authAPIKeyFn security.AuthAPIKeyFunc) goa.Endpoint {
return func(ctx context.Context, req any) (any, error) {
p := req.(*DoublySecurePayload)
// Validate JWT first
ctx, err = authJWTFn(ctx, p.Token, &sc)
if err == nil {
// Then validate API key
ctx, err = authAPIKeyFn(ctx, p.Key, &sc)
}
if err != nil {
return nil, err
}
// Call service method if both auth checks pass
return s.DoublySecure(ctx, p)
}
}