JWT Authentication
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.
How JWT Auth Works
- Client authenticates and receives a JWT
- JWT is included in subsequent requests (usually in Authorization header)
- Server validates the JWT signature and claims
- If valid, the request is processed with the claims’ context
For a detailed explanation of the JWT authentication flow, see the JWT Authentication Flow Guide.
JWT Structure
A JWT consists of three parts (see JWT.io Debugger for live examples):
- Header (algorithm & token type)
- Payload (claims)
- Signature
Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
For more information about JWT claims, see the JWT Claims Documentation.
Understanding Scopes
What Are Scopes?
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:
- A mobile app might have
readscope to view data - An admin dashboard might have both
readandwritescopes - A backup service might have
backupscope
How Scopes Work
- Definition: Scopes are defined in your security scheme
- Assignment: When generating a token, you include the granted scopes
- Validation: When processing a request, you verify the token has the required scopes
Here’s a real-world analogy:
- A hotel key card (JWT) might have different access levels (scopes):
room:access- Access to your room onlypool:access- Access to the swimming poolgym:access- Access to the gymall:access- Full access to all facilities
Scope Format
Scopes 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
Scope Inheritance
Scopes can be hierarchical. For example:
- If a method requires
api:read, a token withadmin:*might also be valid - If a method requires multiple scopes, the token must have ALL required scopes
Example 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
Implementing Scopes in Goa
1. Define Available Scopes
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")
})
2. Apply Scopes to Methods
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")
})
})
})
3. Include Scopes in Tokens
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))
}
4. Validate Scopes
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
}
Best Practices for Scopes
Naming Convention
- Use consistent patterns (
resource:action) - Keep names lowercase and use colons as separators
- Be descriptive but concise
- Use consistent patterns (
Granularity
- Make scopes specific enough for fine-grained control
- But not so specific that they become unmanageable
- Consider grouping related actions
Documentation
- Document what each scope allows
- Provide examples of when to use each scope
- Explain any scope hierarchies
Security
- Always validate scopes on the server
- Don’t trust client-side scope checking
- Consider scope expiration with tokens
Management
- Implement scope rotation for sensitive operations
- Monitor scope usage
- Regularly audit scope assignments
Implementing JWT Auth in Goa
1. Define the Security Scheme
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")
})
2. Apply the Security Scheme
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
})
})
3. Define the Payload
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")
})
})
4. Implement the Security Handler
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
}
Best Practices for JWT Auth
For comprehensive JWT security best practices, see the OWASP JWT Security Cheat Sheet.
1. Token Generation
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))
}
2. Token Validation
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
}
3. Token Refresh
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)
})
})
Generated Code
Goa generates several components for JWT auth:
Security Types
- JWT token types
- Scope validation
- Error types
Middleware
- Token extraction
- Scope validation
- Error handling
OpenAPI Documentation
- Security schemes
- Scope requirements
- Error responses
Common Issues and Solutions
1. Token Validation Errors
Common token validation issues:
- Expired tokens
- Invalid signatures
- Wrong algorithm
- Missing required claims
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
}
2. Scope Validation
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
}
3. Token Refresh Strategy
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)
}
Next Steps
- Learn about OAuth2 Authentication
- Explore API Key Authentication
- Read about Security Best Practices