Skip to content

Authentication

How to authenticate SSE connections with JWT tokens.

Overview

RevenProx uses JWT (JSON Web Tokens) for authentication:

  1. Client includes JWT in request header
  2. Proxy verifies JWT via webhook
  3. Verified tokens are cached
  4. Connection is established or rejected

Sending Authentication

Include the JWT token in the Authorization header:

GET /events/my-topic HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

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

curl -N "http://localhost:8080/events/topic" \
  -H "Authorization: Bearer $JWT_TOKEN"

Go

req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer " + jwtToken)

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:

{
  "valid": true,
  "user_id": "user123",
  "expires_at": 1735689600
}

Failure:

{
  "valid": false,
  "user_id": null,
  "expires_at": null
}

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:

[jwt_verifier]
cache_ttl_sec = 300
cache_max_size = 10000

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:

[jwt_verifier]
webhook_url = ""
require_authentication = false

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

  1. Use HTTPS for all connections
  2. Short token lifetime (15-60 minutes)
  3. Implement refresh tokens for long-lived sessions
  4. Validate all claims in webhook
  5. Use strong signing keys (256+ bits)
  6. Rotate keys periodically
  7. Log authentication failures for security monitoring

Next Steps