# 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 ```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 ```bash # 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` ```go func Encrypt(auth app.ExtAuth) (string, error) ``` **Usage**: ```go 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` ```go func Validate(token string, authServices map[string]AuthorizedService) (*AuthorizedService, error) ``` **Usage**: ```go service, err := jwttoken.Validate(jwtToken, config.AuthorizedServices) // Returns authorized service with token and permissions ``` ### Middleware **Location**: `src/client/api/rest/midlewares.go` ```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 ```json { "iss": "Skeleton", // Issuer (who created the token) "token": "franco:bpa31kniubroq28rpvg010", // User credentials "iat": 1704481490 // Issued at (Unix timestamp) } ``` ### Header ```json { "alg": "HS256", // Algorithm (HMAC SHA-256) "typ": "JWT" // Type } ``` --- ## Service Permissions Defined in `src/app/model.go`: ```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**: ```go 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 ```bash # 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 ```bash # 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): ```toml [External.QApix] Token = "franco:password" Permissions = ["QApixAccess"] # ❌ Sender declares own permissions ``` **After** (recipient-controlled): ```toml [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): ```json { "status": "ok", "build": "feature-branch", "sha": "abc123def" } ``` **When `EnableJWTAuth = true`** (production): ```json { "status": "ok", "build": "feature-branch", "sha": "abc123def", "jwtAuthentications": "ok" } ``` **When JWT validation fails** (`EnableJWTAuth = true`): ```json { "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 - [JWT RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) - [golang-jwt/jwt](https://github.com/golang-jwt/jwt) - Internal: `tools/README-JWT-GENERATOR.md`