feat: Add comprehensive guide on JWT token security, including structure, vulnerabilities, best practices, and revocation logic for LLM plugins.

This commit is contained in:
shiva108 committed 2025-12-30 16:42:59 +01:00
1 parent 9f4587c4e8
commit 140538016d
1 file changed
+305 -5
+305 -5
View File
@@ -1002,6 +1002,300 @@ def test_oauth_flow():
## JWT token security
**Understanding JWT for LLM Plugins:**
JSON Web Tokens (JWT) are self-contained tokens that carry authentication and authorization information. Unlike session IDs that require database lookups, JWTs are stateless—all necessary data is encoded in the token itself. This makes them ideal for distributed LLM plugin systems where centralized session storage would be a bottleneck.
**JWT Structure:**
A JWT consists of three parts separated by dots:
```
header.payload.signature
```
Example:
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInBlcm1pc3Npb25zIjpbInJlYWQiXX0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
```
**Decoded:**
1. **Header** (Base64-encoded JSON):
```json
{ "alg": "HS256", "typ": "JWT" }
```
- `alg`: Signing algorithm (HMAC SHA256)
- `typ`: Token type
2. **Payload** (Base64-encoded JSON):
```json
{
"user_id": 123,
"permissions": ["read"],
"iat": 1640000000,
"exp": 1640086400,
"jti": "unique-token-id"
}
```
- `user_id`: User identifier
- `permissions`: Authorization claims
- `iat`: Issued at (Unix timestamp)
- `exp`: Expiration (Unix timestamp)
- `jti`: JWT ID (for revocation)
3. **Signature** (Cryptographic hash):
```
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
```
**Why JWTs are Useful:**
✅ **Stateless**: No database lookup required for validation
✅ **Scalable**: Can be validated by any server with the secret key
✅ **Self-Contained**: All user info embedded in token
✅ **Cross-Domain**: Works across different services/plugins
✅ **Standard**: RFC 7519, widely supported
**How This Implementation Works:**
**1. Token Creation:**
```python
def create_token(self, user_id, permissions, expiration_hours=24):
payload = {
'user_id': user_id,
'permissions': permissions,
'iat': time.time(), # When token was issued
'exp': time.time() + (expiration_hours * 3600), # When it expires
'jti': secrets.token_urlsafe(16) # Unique token ID
}
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
return token
```
**Key claims explained:**
- **iat (Issued At)**: Prevents token replay attacks from the past
- **exp (Expiration)**: Limits token lifetime (typically 1-24 hours)
- **jti (JWT ID)**: Unique identifier for token revocation (stored in blacklist)
**2. Token Validation:**
```python
def validate_token(self, token):
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm] # CRITICAL: Specify allowed algorithms
)
```
**Why `algorithms=[self.algorithm]` is critical:**
Without this, attacker can change `alg` in header to `none` or `HS256` when expecting `RS256`, bypassing signature verification entirely. This is called the **algorithm confusion attack**.
**Algorithm Confusion Attack Example:**
```python
# Vulnerable code (no algorithm specification)
payload = jwt.decode(token, secret_key) # ❌ DANGEROUS
# Attacker creates token with alg=none:
malicious_token = base64_encode('{"alg":"none"}') + '.' + base64_encode('{"user_id":1,"permissions":["admin"]}') + '.'
# Server accepts it because no algorithm was enforced!
# Result: Attacker has admin access without valid signature
```
**Secure version:**
```python
payload = jwt.decode(token, secret_key, algorithms=['HS256']) # ✅ SAFE
# If token uses different algorithm → InvalidTokenError
```
**3. Expiration Check:**
```python
if payload['exp'] < time.time():
raise TokenExpiredError()
```
Even if signature is valid, reject expired tokens. This limits the damage if a token is stolen—it only works until expiration.
**4. Revocation Check:**
```python
if self.is_token_revoked(payload['jti']):
raise TokenRevokedError()
```
JWTs are stateless, but you can maintain a blacklist of revoked `jti` values (in Redis or database). This allows manual token revocation when:
- User logs out
- Account is compromised
- Permissions change
**Common JWT Vulnerabilities:**
**1. Algorithm Confusion (alg=none)**
- **Attack**: Change `alg` to `none`, remove signature
- **Defense**: Always specify `algorithms` parameter in decode
**2. Weak Secret Keys**
```python
# ❌ Bad: Easily brute-forced
secret_key = "secret123"
# ✅ Good: Strong random key
secret_key = secrets.token_urlsafe(64)
```
**3. No Expiration**
```python
# ❌ Bad: Token never expires
payload = {'user_id': 123} # Missing 'exp'
# ✅ Good: Short expiration
payload = {'user_id': 123, 'exp': time.time() + 3600} # 1 hour
```
**4. Storing Sensitive Data**
```python
# ❌ Bad: JWT payloads are Base64-encoded, NOT encrypted
payload = {'user_id': 123, 'password': 'secret123'} # Visible to anyone!
# ✅ Good: Only non-sensitive data
payload = {'user_id': 123, 'permissions': ['read']}
```
**5. Not Validating Claims**
```python
# ❌ Bad: Accept any valid JWT
payload = jwt.decode(token, secret_key, algorithms=['HS256'])
# ✅ Good: Validate issuer, audience
payload = jwt.decode(
token,
secret_key,
algorithms=['HS256'],
issuer='myapp.com', # Only accept tokens from our app
audience='api.myapp.com' # Only for our API
)
```
**Security Best Practices:**
1. **Use strong cryptographic secrets**:
```python
import secrets
SECRET_KEY = secrets.token_urlsafe(64) # 512 bits of entropy
```
2. **Short expiration times**:
```python
'exp': time.time() + 900 # 15 minutes for access tokens
```
Use refresh tokens for longer sessions.
3. **Rotate secrets regularly**:
```python
# Support multiple keys for rotation
KEYS = {
'key1': 'old-secret',
'key2': 'current-secret'
}
# Try all keys when validating
for key_id, key in KEYS.items():
try:
return jwt.decode(token, key, algorithms=['HS256'])
except jwt.InvalidTokenError:
continue
```
4. **Include audience and issuer**:
```python
payload = {
'iss': 'myapp.com', # Issuer
'aud': 'api.myapp.com', # Audience
'sub': 'user123', # Subject (user ID)
'exp': time.time() + 3600
}
```
5. **Use RS256 for public/private key scenarios**:
```python
# When multiple services need to validate tokens
# but shouldn't be able to create them
# Token creation (private key)
token = jwt.encode(payload, private_key, algorithm='RS256')
# Token validation (public key)
payload = jwt.decode(token, public_key, algorithms=['RS256'])
```
**HS256 vs RS256:**
| Feature | HS256 (HMAC) | RS256 (RSA) |
| ----------- | ------------------------- | ---------------------------------- |
| Key Type | Shared secret | Public/Private keypair |
| Use Case | Single service | Multiple services |
| Signing | Same key signs & verifies | Private key signs, public verifies |
| Security | Secret must be protected | Private key must be protected |
| Performance | Faster | Slower (asymmetric crypto) |
**When to use RS256:**
- Multiple plugins need to validate tokens
- Don't want to share secret with all plugins
- Public key distribution is acceptable
**Token Storage:**
```python
# ✅ Good: HTTP-only cookie (not accessible via JavaScript)
response.set_cookie(
'jwt_token',
token,
httponly=True, # Prevents XSS attacks
secure=True, # HTTPS only
samesite='Strict' # CSRF protection
)
# ❌ Bad: localStorage (vulnerable to XSS)
localStorage.setItem('jwt_token', token) # JavaScript can access!
```
**Prerequisites:**
- Understanding of cryptographic signatures
- Familiarity with Base64 encoding
- Knowledge of token-based authentication
- Awareness of common JWT vulnerabilities
```python
import jwt
import time
@@ -1012,6 +1306,7 @@ class JWTTokenManager:
def __init__(self, secret_key, algorithm='HS256'):
self.secret_key = secret_key
self.algorithm = algorithm
self.revocation_list = set() # Initialize revocation list
def create_token(self, user_id, permissions, expiration_hours=24):
"""Create JWT token"""
@@ -1035,17 +1330,22 @@ class JWTTokenManager:
algorithms=[self.algorithm]
)
# Check if token is revoked
if self.is_revoked(payload['jti']):
# Check expiration
if payload['exp'] < time.time():
raise TokenExpiredError()
# Verify not revoked
if self.is_token_revoked(payload['jti']):
raise TokenRevokedError()
return payload
except jwt.ExpiredSignatureError:
raise TokenExpiredError()
except jwt.InvalidTokenError:
raise InvalidTokenError()
def is_token_revoked(self, jti):
"""Check if a token is in the revocation list"""
return jti in self.revocation_list
def revoke_token(self, jti):
"""Revoke specific token"""
self.revocation_list.add(jti)