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 enabledtrue: Enables JWT authentication and health check validationfalse: 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_KEYon Skeleton =SKELETON_QUANTEX_SECRET_KEYon QApixQAPIX_QUANTEX_SECRET_KEYon Skeleton =QAPIX_QUANTEX_SECRET_KEYon 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:
- Receives JWT from Authorization header
- Calls
jwttoken.Validate()to verify JWT and check permissions - Calls
validateServiceToken(token)to verify user credentials in database - Sets
authorized_servicein Gin context - 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
AuthorizedServicesin conf.toml - Verify
Namematches JWTissclaim
"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
Permissionsarray 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
- Update
conf.tomlon both services- Set
EnableJWTAuth = truefor production environments - Set
EnableJWTAuth = falsefor local development - Configure
ExternalandAuthorizedServicessections
- Set
- Set environment variables for secret keys
- Restart services
- Test with
generate-jwt.sh - Verify health endpoint shows
jwtAuthentications: "ok" - Monitor logs for authentication errors
Security Best Practices
- Rotate Secret Keys Regularly - Update environment variables periodically
- Use Strong Secrets - Minimum 32 bytes, cryptographically random
- HTTPS Only - Never send JWTs over unencrypted connections
- Monitor Auth Logs - Track failed authentication attempts
- Principle of Least Privilege - Use ReadOnly when possible
- 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:
- Attempting to fetch a test user from the database
- Verifying the database connection is working
- Returning
jwtAuthentications: "ok"if successful - Returning
jwtAuthentications: "error"andstatus: "degraded"if validation fails
Location: src/client/api/rest/controller.go:178
Additional Resources
- JWT RFC 7519
- golang-jwt/jwt
- Internal:
tools/README-JWT-GENERATOR.md