JSON Web Tokens (JWT) provide a secure way to transmit claims between parties. They’re particularly useful in microservices architectures where you need to pass authentication and authorization information between services. JWTs are self-contained tokens that can include user information, permissions, and other claims.
For a detailed explanation of the JWT authentication flow, see the JWT Authentication Flow Guide.
A JWT consists of three parts (see JWT.io Debugger for live examples):
Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
For more information about JWT claims, see the JWT Claims Documentation.
Scopes are permissions that determine what actions a client can perform with an API. Think of scopes as a way to implement granular access control. For example:
read
scope to view dataread
and write
scopesbackup
scopeHere’s a real-world analogy:
room:access
- Access to your room onlypool:access
- Access to the swimming poolgym:access
- Access to the gymall:access
- Full access to all facilitiesScopes typically follow a pattern like resource:action
. Common examples:
api:read # Read-only access to API
api:write # Write access to API
users:create # Ability to create users
admin:* # Full admin access
Scopes can be hierarchical. For example:
api:read
, a token with admin:*
might also be validExample of scope hierarchy:
admin:* # Full admin access (includes all admin scopes)
├── admin:read # Read admin resources
├── admin:write # Modify admin resources
└── admin:delete # Delete admin resources
First, define what scopes exist in your API:
var JWTAuth = JWTSecurity("jwt", func() {
Description("JWT authentication with scopes")
// Define all available scopes
Scope("api:read", "Read access to API resources")
Scope("api:write", "Write access to API resources")
Scope("api:admin", "Full administrative access")
Scope("users:read", "Read user profiles")
Scope("users:write", "Modify user profiles")
})
Then, specify which scopes are required for each endpoint:
var _ = Service("users", func() {
// List users - requires read access
Method("list", func() {
Security(JWTAuth, func() {
// Only needs read access
Scope("users:read")
})
})
// Update user - requires write access
Method("update", func() {
Security(JWTAuth, func() {
// Needs both read and write access
Scope("users:read", "users:write")
})
})
// Delete user - requires admin access
Method("delete", func() {
Security(JWTAuth, func() {
Scope("api:admin")
})
})
})
When generating tokens, include the granted scopes:
func GenerateUserToken(user *User) (string, error) {
// Determine scopes based on user role
var scopes []string
switch user.Role {
case "admin":
scopes = []string{"api:admin", "users:read", "users:write"}
case "editor":
scopes = []string{"users:read", "users:write"}
default:
scopes = []string{"users:read"}
}
claims := Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
IssuedAt: time.Now().Unix(),
Subject: user.ID,
},
Scopes: scopes, // Include scopes in token
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}
When processing requests, validate that the token has the required scopes:
func validateScopes(tokenScopes []string, requiredScopes []string) error {
// Create a map of the token's scopes for efficient lookup
scopeMap := make(map[string]bool)
for _, scope := range tokenScopes {
scopeMap[scope] = true
}
// Special case: admin scope grants all access
if scopeMap["api:admin"] {
return nil
}
// Check each required scope
for _, required := range requiredScopes {
if !scopeMap[required] {
return fmt.Errorf("missing required scope: %s", required)
}
}
return nil
}
Naming Convention
resource:action
)Granularity
Documentation
Security
Management
First, define your JWT security scheme in your design package.
package design
import (
. "goa.design/goa/v3/dsl"
)
// JWTAuth defines our security scheme
var JWTAuth = JWTSecurity("jwt", func() {
Description("JWT authentication")
// Define scopes for authorization
Scope("api:read", "Read access to API")
Scope("api:write", "Write access to API")
})
JWT auth can be applied at different levels with specific scope requirements.
// API level - applies to all services and methods
var _ = API("secure_api", func() {
Security(JWTAuth, func() {
Scope("api:read") // Default minimum scope
})
})
// Service level - applies to all methods in the service
var _ = Service("secure_service", func() {
Security(JWTAuth, func() {
Scope("api:write") // Require write scope
})
})
// Method level - applies only to this method
Method("secure_method", func() {
Security(JWTAuth, func() {
Scope("api:read", "api:write") // Require both scopes
})
})
For methods that use JWT auth, include the token in the payload.
Method("getData", func() {
Security(JWTAuth, func() {
Scope("api:read")
})
Payload(func() {
Token("token", String, func() {
Description("JWT used for authentication")
})
Required("token")
// Additional payload fields
Field(1, "query", String, "Search query")
})
Result(ArrayOf(String))
HTTP(func() {
GET("/data")
// Map the token to the Authorization header
Header("token:Authorization")
})
})
When Goa generates the code, you’ll need to implement a JWT security handler. This example uses the golang-jwt/jwt library, which is the recommended JWT library for Go.
// SecurityJWTFunc implements the authorization logic for JWT auth
func (s *service) JWTAuth(ctx context.Context, token string,
scheme *security.JWTScheme) (context.Context, error) {
// Parse and validate the JWT
claims, err := s.parseAndValidateJWT(token)
if err != nil {
return ctx, jwt.Unauthorized("invalid token")
}
// Validate required scopes
if !hasRequiredScopes(claims.Scopes, scheme.RequiredScopes) {
return ctx, jwt.Unauthorized("insufficient scopes")
}
// Add claims to context
ctx = context.WithValue(ctx, "jwt_claims", claims)
return ctx, nil
}
func (s *service) parseAndValidateJWT(token string) (*Claims, error) {
// Parse the JWT using your preferred library
// Example using golang-jwt/jwt:
claims := &Claims{}
parsedToken, err := jwt.ParseWithClaims(token, claims,
func(token *jwt.Token) (interface{}, error) {
// Validate signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v",
token.Header["alg"])
}
return []byte(s.config.JWTSecret), nil
})
if err != nil || !parsedToken.Valid {
return nil, err
}
return claims, nil
}
// Claims defines your custom JWT claims
type Claims struct {
jwt.StandardClaims
UserID string `json:"uid"`
Scopes []string `json:"scopes"`
}
func hasRequiredScopes(tokenScopes, requiredScopes []string) bool {
scopeMap := make(map[string]bool)
for _, scope := range tokenScopes {
scopeMap[scope] = true
}
for _, required := range requiredScopes {
if !scopeMap[required] {
return false
}
}
return true
}
For comprehensive JWT security best practices, see the OWASP JWT Security Cheat Sheet.
Generate JWTs with appropriate claims and expiration. For more information about JWT signing methods, see the JWT Signing Algorithms Overview.
func GenerateJWT(userID string, scopes []string) (string, error) {
claims := Claims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "your-api",
},
UserID: userID,
Scopes: scopes,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}
Implement comprehensive token validation following the JWT Best Practices RFC:
func ValidateToken(tokenString string) (*Claims, error) {
// Parse the token
token, err := jwt.ParseWithClaims(tokenString, &Claims{},
func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHS256); !ok {
return nil, fmt.Errorf("unexpected signing method: %v",
token.Header["alg"])
}
return []byte(jwtSecret), nil
})
if err != nil {
return nil, err
}
// Type assert the claims
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
// Additional validation
if err := validateCustomClaims(claims); err != nil {
return nil, err
}
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func validateCustomClaims(claims *Claims) error {
// Validate issuer
if claims.Issuer != "your-api" {
return fmt.Errorf("invalid issuer")
}
// Validate other custom requirements
return nil
}
Implement token refresh to maintain user sessions. For more information about refresh tokens, see the Auth0 Refresh Token Guide.
Method("refresh", func() {
Description("Refresh an existing JWT token")
Security(JWTAuth)
Payload(func() {
Token("token", String)
Required("token")
})
Result(func() {
Field(1, "token", String, "New JWT token")
Field(2, "expires_at", String, "Token expiration time")
Required("token", "expires_at")
})
HTTP(func() {
POST("/auth/refresh")
Response(StatusOK)
Response(StatusUnauthorized)
})
})
Goa generates several components for JWT auth:
Security Types
Middleware
OpenAPI Documentation
Common token validation issues:
Solution: Implement comprehensive validation:
func validateToken(token *jwt.Token) error {
if err := validateSignature(token); err != nil {
return err
}
if err := validateExpiration(token); err != nil {
return err
}
if err := validateClaims(token); err != nil {
return err
}
return nil
}
Ensure proper scope checking:
func validateScopes(tokenScopes []string, requiredScopes []string) error {
scopeMap := make(map[string]bool)
for _, scope := range tokenScopes {
scopeMap[scope] = true
}
for _, required := range requiredScopes {
if !scopeMap[required] {
return fmt.Errorf("missing required scope: %s", required)
}
}
return nil
}
Implement a robust refresh strategy:
func refreshToken(oldToken string) (string, error) {
// Validate old token
claims, err := validateToken(oldToken)
if err != nil {
return "", err
}
// Check if refresh is allowed
if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) >
time.Hour*24*7 {
return "", fmt.Errorf("token too old to refresh")
}
// Generate new token
return GenerateJWT(claims.UserID, claims.Scopes)
}