Authentication
How to authenticate SSE connections with JWT tokens.
Overview
RevenProx uses JWT (JSON Web Tokens) for authentication:
- Client includes JWT in request header
- Proxy verifies JWT via webhook
- Verified tokens are cached
- Connection is established or rejected
Sending Authentication
Include the JWT token in the Authorization header:
JavaScript
// Standard EventSource doesn't support custom headers
// Use a polyfill or wrapper library
// Option 1: Query parameter (less secure)
const url = new URL('http://localhost:8080/events/topic');
url.searchParams.set('token', jwtToken);
const eventSource = new EventSource(url);
// Option 2: Use fetch with ReadableStream
async function connectSSE(topic, token) {
const response = await fetch(`/events/${topic}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'text/event-stream'
}
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE events from text
console.log(text);
}
}
// Option 3: Use event-source-polyfill
import { EventSourcePolyfill } from 'event-source-polyfill';
const eventSource = new EventSourcePolyfill('/events/topic', {
headers: {
'Authorization': `Bearer ${jwtToken}`
}
});
Python
import requests
headers = {
'Authorization': f'Bearer {jwt_token}',
'Accept': 'text/event-stream'
}
response = requests.get(
'http://localhost:8080/events/topic',
headers=headers,
stream=True
)
curl
Go
JWT Token Structure
Tokens should contain standard claims:
{
"sub": "user123",
"iss": "auth.example.com",
"aud": "revenprox",
"exp": 1735689600,
"iat": 1735686000,
"permissions": ["topic:read", "topic:write"]
}
Required Claims
| Claim | Description |
|---|---|
sub |
Subject (user ID) |
exp |
Expiration timestamp |
Optional Claims
| Claim | Description |
|---|---|
iss |
Token issuer |
aud |
Intended audience |
iat |
Issued at timestamp |
Webhook Verification
Your webhook validates tokens and authorizes topic access.
Request to Webhook
POST /verify HTTP/1.1
Content-Type: application/json
{
"jwt": "eyJhbGciOiJIUzI1NiIs...",
"topic": "a1b2c3d4e5f67890abcdef1234567890"
}
Expected Response
Success:
Failure:
Topic Authorization
Implement topic-level authorization in your webhook:
app.post('/verify', (req, res) => {
const { jwt: token, topic } = req.body;
try {
const decoded = jwt.verify(token, JWT_SECRET);
// Check if user can access this topic
if (!canAccessTopic(decoded.sub, topic)) {
return res.json({ valid: false, user_id: null, expires_at: null });
}
res.json({
valid: true,
user_id: decoded.sub,
expires_at: decoded.exp
});
} catch (error) {
res.json({ valid: false, user_id: null, expires_at: null });
}
});
function canAccessTopic(userId, topicHex) {
// Example: Check user's topic permissions
const userTopics = getUserTopics(userId);
return userTopics.some(t => topicToHex(t) === topicHex);
}
Token Caching
Verified tokens are cached to reduce webhook load:
- Cache key: SHA-256 hash of JWT
- TTL:
cache_ttl_sec(default 300s) - Max entries:
cache_max_size(default 10000)
Configure caching:
Token Refresh
Handle token expiration gracefully:
class SSEConnection {
constructor(topic, getToken) {
this.topic = topic;
this.getToken = getToken;
this.connect();
}
connect() {
const token = this.getToken();
this.source = new EventSourcePolyfill(`/events/${this.topic}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
this.source.onerror = (e) => {
if (e.status === 401) {
// Token expired, get new one and reconnect
this.source.close();
setTimeout(() => this.connect(), 1000);
}
};
}
}
Disabling Authentication
For development only:
Danger
Never disable authentication in production environments.
Error Handling
401 Unauthorized
Causes:
- Missing Authorization header
- Malformed header (not Bearer <token>)
- Invalid JWT signature
- Expired token
- Webhook returned valid: false
Client handling:
eventSource.onerror = (e) => {
if (e.status === 401) {
// Redirect to login or refresh token
redirectToLogin();
}
};
Circuit Breaker Open
When the webhook fails repeatedly, authentication is temporarily unavailable:
// Retry with exponential backoff
function connectWithRetry(topic, token, attempt = 0) {
const source = new EventSource(...);
source.onerror = (e) => {
if (e.status === 503) {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
setTimeout(() => connectWithRetry(topic, token, attempt + 1), delay);
}
};
}
Security Best Practices
- Use HTTPS for all connections
- Short token lifetime (15-60 minutes)
- Implement refresh tokens for long-lived sessions
- Validate all claims in webhook
- Use strong signing keys (256+ bits)
- Rotate keys periodically
- Log authentication failures for security monitoring