Enterprise API Gateway
Part 2: Authentication & Authorization
• software • active • Part 2 of 4
Technologies:
GoEnvoygRPCKubernetesPrometheus
Building on our core gateway, we’ll now implement a comprehensive security layer with JWT validation, OAuth2 integration, and role-based access control.
Security Architecture
Our security implementation includes:
- JWT Token Validation: Verify and decode JWT tokens
- OAuth2 Integration: Support for multiple OAuth2 providers
- RBAC (Role-Based Access Control): Fine-grained permissions
- API Key Management: Support for API keys alongside tokens
JWT Authentication Service
First, let’s implement a JWT validation service:
// internal/auth/jwt.go
package auth
import (
"crypto/rsa"
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
)
type JWTValidator struct {
publicKey *rsa.PublicKey
issuer string
audience string
}
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Roles []string `json:"roles"`
Scopes []string `json:"scopes"`
jwt.RegisteredClaims
}
func NewJWTValidator(publicKey *rsa.PublicKey, issuer, audience string) *JWTValidator {
return &JWTValidator{
publicKey: publicKey,
issuer: issuer,
audience: audience,
}
}
func (v *JWTValidator) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return v.publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
// Validate issuer and audience
if claims.Issuer != v.issuer {
return nil, fmt.Errorf("invalid issuer")
}
if !claims.VerifyAudience(v.audience, true) {
return nil, fmt.Errorf("invalid audience")
}
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
OAuth2 Integration
We implement OAuth2 support for multiple providers:
// internal/auth/oauth2.go
package auth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/github"
)
type OAuth2Provider interface {
GetConfig() *oauth2.Config
GetUserInfo(ctx context.Context, token *oauth2.Token) (*UserInfo, error)
}
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
Provider string `json:"provider"`
}
type GoogleProvider struct {
config *oauth2.Config
}
func NewGoogleProvider(clientID, clientSecret, redirectURL string) *GoogleProvider {
return &GoogleProvider{
config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: []string{"openid", "email", "profile"},
Endpoint: google.Endpoint,
},
}
}
func (p *GoogleProvider) GetConfig() *oauth2.Config {
return p.config
}
func (p *GoogleProvider) GetUserInfo(ctx context.Context, token *oauth2.Token) (*UserInfo, error) {
client := p.config.Client(ctx, token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var googleUser struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
if err := json.NewDecoder(resp.Body).Decode(&googleUser); err != nil {
return nil, err
}
return &UserInfo{
ID: googleUser.ID,
Email: googleUser.Email,
Name: googleUser.Name,
Picture: googleUser.Picture,
Provider: "google",
}, nil
}
Role-Based Access Control
Implement RBAC with fine-grained permissions:
// internal/auth/rbac.go
package auth
import (
"context"
"fmt"
)
type Permission string
const (
ReadUsers Permission = "users:read"
WriteUsers Permission = "users:write"
ReadOrders Permission = "orders:read"
WriteOrders Permission = "orders:write"
AdminPanel Permission = "admin:access"
)
type Role struct {
Name string
Permissions []Permission
}
type RBACService struct {
roles map[string]*Role
}
func NewRBACService() *RBACService {
rbac := &RBACService{
roles: make(map[string]*Role),
}
// Define default roles
rbac.roles["user"] = &Role{
Name: "user",
Permissions: []Permission{ReadUsers},
}
rbac.roles["customer"] = &Role{
Name: "customer",
Permissions: []Permission{ReadUsers, ReadOrders},
}
rbac.roles["admin"] = &Role{
Name: "admin",
Permissions: []Permission{
ReadUsers, WriteUsers,
ReadOrders, WriteOrders,
AdminPanel,
},
}
return rbac
}
func (r *RBACService) CheckPermission(userRoles []string, required Permission) bool {
for _, roleName := range userRoles {
if role, exists := r.roles[roleName]; exists {
for _, perm := range role.Permissions {
if perm == required {
return true
}
}
}
}
return false
}
func (r *RBACService) GetUserPermissions(userRoles []string) []Permission {
var permissions []Permission
seen := make(map[Permission]bool)
for _, roleName := range userRoles {
if role, exists := r.roles[roleName]; exists {
for _, perm := range role.Permissions {
if !seen[perm] {
permissions = append(permissions, perm)
seen[perm] = true
}
}
}
}
return permissions
}
Envoy External Auth Filter
We integrate our auth service with Envoy’s external auth filter:
// internal/auth/server.go
package auth
import (
"context"
"strings"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type AuthServer struct {
jwtValidator *JWTValidator
rbacService *RBACService
}
func NewAuthServer(jwtValidator *JWTValidator, rbacService *RBACService) *AuthServer {
return &AuthServer{
jwtValidator: jwtValidator,
rbacService: rbacService,
}
}
func (s *AuthServer) Check(ctx context.Context, req *auth.CheckRequest) (*auth.CheckResponse, error) {
// Extract authorization header
authHeader := ""
if req.Attributes.Request.Http.Headers != nil {
authHeader = req.Attributes.Request.Http.Headers["authorization"]
}
if authHeader == "" {
return &auth.CheckResponse{
Status: &status.Status{Code: int32(codes.Unauthenticated)},
}, nil
}
// Remove "Bearer " prefix
token := strings.TrimPrefix(authHeader, "Bearer ")
// Validate JWT token
claims, err := s.jwtValidator.ValidateToken(token)
if err != nil {
return &auth.CheckResponse{
Status: &status.Status{Code: int32(codes.Unauthenticated)},
}, nil
}
// Check permissions based on the requested path
path := req.Attributes.Request.Http.Path
required := s.getRequiredPermission(path)
if required != "" && !s.rbacService.CheckPermission(claims.Roles, required) {
return &auth.CheckResponse{
Status: &status.Status{Code: int32(codes.PermissionDenied)},
}, nil
}
// Add user info to headers for upstream services
headers := map[string]string{
"x-user-id": claims.UserID,
"x-user-email": claims.Email,
"x-user-roles": strings.Join(claims.Roles, ","),
}
return &auth.CheckResponse{
Status: &status.Status{Code: int32(codes.OK)},
HttpResponse: &auth.CheckResponse_OkResponse{
OkResponse: &auth.OkHttpResponse{
Headers: []*core.HeaderValueOption{},
},
},
}, nil
}
func (s *AuthServer) getRequiredPermission(path string) Permission {
switch {
case strings.HasPrefix(path, "/api/v1/admin"):
return AdminPanel
case strings.HasPrefix(path, "/api/v1/users") && strings.Contains(path, "POST"):
return WriteUsers
case strings.HasPrefix(path, "/api/v1/users"):
return ReadUsers
case strings.HasPrefix(path, "/api/v1/orders") && strings.Contains(path, "POST"):
return WriteOrders
case strings.HasPrefix(path, "/api/v1/orders"):
return ReadOrders
default:
return ""
}
}
Testing Authentication
Test the authentication system:
# Get a JWT token
TOKEN=$(curl -s -X POST http://auth.example.com/oauth2/token \
-d "grant_type=authorization_code&code=xyz")
# Make authenticated request
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/v1/users
# Test unauthorized access
curl http://localhost:8080/api/v1/admin/users
# Should return 401 Unauthorized
Next Steps
In Part 3, we’ll implement rate limiting and circuit breaking to ensure our API gateway can handle high loads and gracefully degrade when upstream services are unhealthy.
Key topics will include:
- Distributed rate limiting with Redis
- Circuit breaker patterns
- Bulkhead isolation
- Graceful degradation strategies