Files
qfixpt/docs/JWT_SERVICE_AUTHENTICATION.md
2026-03-11 10:54:11 -03:00

12 KiB

JWT Service-to-Service Authentication

Overview

Services authenticate with each other using JWT tokens. The system uses two-factor authentication: JWT signature validation + user credential validation against the database.


How It Works

Outgoing Request (Skeleton → QApix)

1. Skeleton wants to call QApix
2. Creates JWT with:
   - iss: "Skeleton"
   - token: "franco:bpa31kniubroq28rpvg010" (from External.QApix.Token in config)
   - iat: current timestamp
3. Signs JWT with QAPIX_QUANTEX_SECRET_KEY
4. Sends request with header: Authorization: {JWT}

Incoming Request (QApix → Skeleton)

1. Skeleton receives JWT from QApix
2. Validates JWT signature with SKELETON_QUANTEX_SECRET_KEY
3. Checks "QApix" is in AuthorizedServices config
4. Checks QApix has required permissions (currently FullAccess)
5. Extracts token claim: "franco:bpa31kniubroq28rpvg010"
6. Looks up user "franco" in database
7. Verifies password "bpa31kniubroq28rpvg010" matches
8. ✅ Request allowed

Configuration

conf.toml

# Enable/disable JWT authentication
EnableJWTAuth = true  # Set to true to enable JWT authentication checks

# Who we call (outgoing requests)
[External.QApix]
Name = "QApix"
Token = "franco:bpa31kniubroq28rpvg010"  # Credentials we use to authenticate
Host = "https://qapix.example.com"
Port = "5001"

# Who can call us (incoming requests)
[AuthorizedServices.QApix]
Name = "QApix"
Permissions = ["FullAccess"]  # What QApix is allowed to do
# Token validated against user DB - not stored in config

Configuration Options:

  • EnableJWTAuth (bool): Controls whether JWT authentication is enabled
    • true: Enables JWT authentication and health check validation
    • false: Disables JWT authentication features (default for local development)

Environment Variables

# On Skeleton
export SKELETON_QUANTEX_SECRET_KEY="..."  # Validates incoming JWTs
export QAPIX_QUANTEX_SECRET_KEY="..."     # Signs outgoing JWTs to QApix

# On QApix
export QAPIX_QUANTEX_SECRET_KEY="..."     # Validates incoming JWTs
export SKELETON_QUANTEX_SECRET_KEY="..."  # Signs outgoing JWTs to Skeleton

Important: Secret keys must match across services:

  • SKELETON_QUANTEX_SECRET_KEY on Skeleton = SKELETON_QUANTEX_SECRET_KEY on QApix
  • QAPIX_QUANTEX_SECRET_KEY on Skeleton = QAPIX_QUANTEX_SECRET_KEY on QApix

Security Layers

Layer Description Purpose
1. JWT Signature Validates HS256 signature Proves caller has secret key
2. Issuer Whitelist Checks AuthorizedServices Only known services allowed
3. Permission Check Validates service permissions ReadOnly/ReadWrite/FullAccess
4. User Validation Checks token against user DB Real credentials required

Code Flow

Creating JWT

Location: src/common/jwttoken/jwt.go

func Encrypt(auth app.ExtAuth) (string, error)

Usage:

auth := app.ExtAuth{
    Name: "QApix",
    Token: "franco:bpa31kniubroq28rpvg010",
}
jwt, err := jwttoken.Encrypt(auth)
// Returns signed JWT token

Validating JWT

Location: src/common/jwttoken/jwt.go

func Validate(token string, authServices map[string]AuthorizedService) (*AuthorizedService, error)

Usage:

service, err := jwttoken.Validate(jwtToken, config.AuthorizedServices)
// Returns authorized service with token and permissions

Middleware

Location: src/client/api/rest/midlewares.go

func (cont *Controller) JWTTokenAuth(reqToken string, ctx *gin.Context)

Flow:

  1. Receives JWT from Authorization header
  2. Calls jwttoken.Validate() to verify JWT and check permissions
  3. Calls validateServiceToken(token) to verify user credentials in database
  4. Sets authorized_service in Gin context
  5. Proceeds to next handler

JWT Token Structure

Claims

{
  "iss": "Skeleton",                    // Issuer (who created the token)
  "token": "franco:bpa31kniubroq28rpvg010",  // User credentials
  "iat": 1704481490                     // Issued at (Unix timestamp)
}

Header

{
  "alg": "HS256",                       // Algorithm (HMAC SHA-256)
  "typ": "JWT"                          // Type
}

Service Permissions

Defined in src/app/model.go:

type ServicePermission int

const (
    ServicePermissionReadOnly    // Read-only access
    ServicePermissionReadWrite   // Read and write access
    ServicePermissionFullAccess  // Full access to all operations
    ServicePermissionUndefined   // No permissions
)

Permission Check:

service.HasPermissions(app.ServicePermissionFullAccess) // Returns bool

Key Benefits

  • No credentials in config - Token validated against user database
  • Recipient controls access - Permissions defined by recipient, not sender
  • Two-factor auth - Secret key + user credentials
  • Easy rotation - Change user password → JWT auth automatically updates
  • Audit trail - Know which service user made each request
  • Single source of truth - User credentials only in database
  • Configurable - Enable/disable JWT auth per environment via EnableJWTAuth

Testing

Generate JWT Token

# Set environment variable
export SKELETON_QUANTEX_SECRET_KEY="your-secret-key"

# Run generator
cd tools
./generate-jwt.sh

# Follow prompts:
# Issuer: Skeleton
# Service: SKELETON
# Token: franco:bpa31kniubroq28rpvg010

Test with curl

# Store generated token
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Test endpoint
curl -H "Authorization: $TOKEN" \
     http://localhost:8097/api/v1/endpoint

# Expected: 200 OK (if token valid and user exists)
# Expected: 401 Unauthorized (if token invalid or user not found)

Test Scenarios

Scenario Expected Result
Valid JWT + Valid user 200 OK
Valid JWT + Invalid user 401 Unauthorized
Invalid JWT signature 401 Unauthorized
Unknown issuer 401 Unauthorized
Insufficient permissions 403 Forbidden

Authentication Flow Diagram

┌─────────────┐                                    ┌─────────────┐
│  Skeleton   │                                    │    QApix    │
└──────┬──────┘                                    └──────┬──────┘
       │                                                  │
       │ 1. Create JWT                                   │
       │    - iss: "Skeleton"                            │
       │    - token: "franco:password"                   │
       │                                                  │
       │ 2. Sign with QAPIX_QUANTEX_SECRET_KEY          │
       │                                                  │
       │ 3. HTTP Request                                 │
       │    Authorization: {JWT}                         │
       ├─────────────────────────────────────────────────>│
       │                                                  │
       │                                    4. Decrypt JWT│
       │                    (QAPIX_QUANTEX_SECRET_KEY)   │
       │                                                  │
       │                              5. Validate claims  │
       │                                    (iss, token)  │
       │                                                  │
       │                         6. Check AuthorizedServices│
       │                                 ("Skeleton" allowed?)│
       │                                                  │
       │                          7. Check permissions   │
       │                              (FullAccess?)      │
       │                                                  │
       │                       8. Validate user in DB    │
       │                          (franco:password exists?)│
       │                                                  │
       │ 9. Response (200 OK or 401/403)                 │
       │<─────────────────────────────────────────────────┤
       │                                                  │

Troubleshooting

Common Issues

"Invalid JWT"

  • Check secret key environment variable is set
  • Verify secret keys match on both services

"Service not authorized"

  • Add service to AuthorizedServices in conf.toml
  • Verify Name matches JWT iss claim

"Invalid credentials"

  • Check user exists in database
  • Verify token format is email:password
  • Ensure password matches user record

"Insufficient permissions"

  • Check service has required permission in AuthorizedServices
  • Update Permissions array in conf.toml

Files Reference

File Purpose
src/common/jwttoken/jwt.go JWT encryption, decryption, validation
src/client/api/rest/midlewares.go HTTP middleware for JWT authentication
src/client/api/rest/controller.go REST controllers including health check with JWT validation
src/app/model.go AuthorizedService, ServicePermission types, EnableJWTAuth config
src/client/api/rest/server.go REST API server configuration
src/cmd/service/service.go Service runner that initializes JWT config
conf.toml Service configuration
tools/generate-jwt.sh JWT token generator for testing

Migration Notes

From Old System

Before (self-declared permissions):

[External.QApix]
Token = "franco:password"
Permissions = ["QApixAccess"]  # ❌ Sender declares own permissions

After (recipient-controlled):

[External.QApix]
Token = "franco:password"
# ✅ No permissions - recipient decides

[AuthorizedServices.QApix]
Permissions = ["FullAccess"]  # ✅ Recipient controls access

Deployment Steps

  1. Update conf.toml on both services
    • Set EnableJWTAuth = true for production environments
    • Set EnableJWTAuth = false for local development
    • Configure External and AuthorizedServices sections
  2. Set environment variables for secret keys
  3. Restart services
  4. Test with generate-jwt.sh
  5. Verify health endpoint shows jwtAuthentications: "ok"
  6. Monitor logs for authentication errors

Security Best Practices

  1. Rotate Secret Keys Regularly - Update environment variables periodically
  2. Use Strong Secrets - Minimum 32 bytes, cryptographically random
  3. HTTPS Only - Never send JWTs over unencrypted connections
  4. Monitor Auth Logs - Track failed authentication attempts
  5. Principle of Least Privilege - Use ReadOnly when possible
  6. Separate Service Users - Don't share user credentials across services

Health Check Endpoint

The /health endpoint provides service health status and optionally includes JWT authentication validation when enabled.

Response Structure

When EnableJWTAuth = false (default for local development):

{
  "status": "ok",
  "build": "feature-branch",
  "sha": "abc123def"
}

When EnableJWTAuth = true (production):

{
  "status": "ok",
  "build": "feature-branch",
  "sha": "abc123def",
  "jwtAuthentications": "ok"
}

When JWT validation fails (EnableJWTAuth = true):

{
  "status": "degraded",
  "build": "feature-branch",
  "sha": "abc123def",
  "jwtAuthentications": "error"
}

Health Check Validation

When EnableJWTAuth = true, the health endpoint validates JWT authentication by:

  1. Attempting to fetch a test user from the database
  2. Verifying the database connection is working
  3. Returning jwtAuthentications: "ok" if successful
  4. Returning jwtAuthentications: "error" and status: "degraded" if validation fails

Location: src/client/api/rest/controller.go:178


Additional Resources