418 lines
12 KiB
Markdown
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`
|