fiumba
This commit is contained in:
417
docs/JWT_SERVICE_AUTHENTICATION.md
Normal file
417
docs/JWT_SERVICE_AUTHENTICATION.md
Normal file
@ -0,0 +1,417 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user