Files
qfixdpl/docs/JWT_SERVICE_AUTHENTICATION.md
Ramiro Paz 0e8fe168ef fiumba
2026-03-09 15:09:06 -03:00

418 lines
12 KiB
Markdown

# 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`