Authentication

Authentication verifies who the user is. Verani handles authentication in the extractMeta function.

Overview

Authentication verifies who the user is. Verani handles authentication in the extractMeta function.

Public Rooms (No Authentication)

export const publicRoom = defineRoom({
  name: "public-room",
  websocketPath: "/ws"
  // Uses default extractMeta
  // userId comes from query param, completely untrusted
});

Security Risks:

  • Users can impersonate anyone
  • No accountability
  • Vulnerable to abuse

Use for:

  • Public demos
  • Anonymous chat rooms
  • Read-only public feeds
  • MVP/prototypes

JWT Authentication (Recommended)

Step 1: Client Gets Token

// Your auth service issues JWT
const response = await fetch("https://your-api.com/login", {
  method: "POST",
  body: JSON.stringify({ username, password })
});

const { token } = await response.json();

Step 2: Client Connects with Token

// Option A: Query parameter (less secure)
const client = new VeraniClient(
  `wss://your-app.dev/ws?token=${token}`
);

// Option B: In first message (more secure, but custom)
const client = new VeraniClient(`wss://your-app.dev/ws`);
await client.waitForConnection();
client.emit("auth", { token });

Note: WebSocket constructor doesn't support custom headers in browsers. For maximum security, use a separate HTTP endpoint to exchange token for a session ID, then connect with that session ID.

Step 3: Server Verifies Token

import { defineRoom } from "verani";

// You'll need a proper JWT library
// npm install @tsndr/cloudflare-worker-jwt
import jwt from "@tsndr/cloudflare-worker-jwt";

interface AuthMeta extends ConnectionMeta {
  email: string;
  role: string;
  verified: boolean;
}

export const secureRoom = defineRoom<AuthMeta>({
  async extractMeta(req) {
    const url = new URL(req.url);
    const token = url.searchParams.get("token");

    if (!token) {
      throw new Error("Authentication required");
    }

    // Verify JWT signature and expiration
    const isValid = await jwt.verify(token, SECRET_KEY);

    if (!isValid) {
      throw new Error("Invalid token");
    }

    const payload = jwt.decode(token);

    // Verify required claims
    if (!payload.sub || !payload.exp) {
      throw new Error("Invalid token claims");
    }

    // Check expiration (jwt.verify should do this, but double-check)
    if (payload.exp < Date.now() / 1000) {
      throw new Error("Token expired");
    }

    return {
      userId: payload.sub,
      clientId: crypto.randomUUID(),
      channels: ["default"],
      email: payload.email,
      role: payload.role || "user",
      verified: true
    };
  },

  onConnect(ctx) {
    // Now you can trust ctx.meta.userId
    console.log(`Verified user ${ctx.meta.userId} connected`);
    
    // Send welcome message using emit API
    ctx.emit.emit("welcome", {
      message: `Welcome, ${ctx.meta.email}!`
    });
  }
});

Session-Based Authentication

If you have session cookies:

export const sessionRoom = defineRoom({
  name: "session-room",
  websocketPath: "/ws",
  
  async extractMeta(req) {
    const sessionId = req.headers.get("Cookie")
      ?.split(";")
      .find(c => c.trim().startsWith("sessionId="))
      ?.split("=")[1];

    if (!sessionId) {
      throw new Error("No session");
    }

    // Validate session (check Redis, KV, etc.)
    const session = await validateSession(sessionId);

    if (!session) {
      throw new Error("Invalid session");
    }

    return {
      userId: session.userId,
      clientId: crypto.randomUUID(),
      channels: ["default"]
    };
  },

  onConnect(ctx) {
    console.log(`User ${ctx.meta.userId} connected via session`);
  }
});

API Key Authentication

For server-to-server or mobile apps:

export const apiKeyRoom = defineRoom({
  name: "api-key-room",
  websocketPath: "/ws",
  
  async extractMeta(req) {
    const apiKey = req.headers.get("X-API-Key");

    if (!apiKey) {
      throw new Error("API key required");
    }

    // Verify API key (check against database/KV)
    const client = await verifyApiKey(apiKey);

    if (!client) {
      throw new Error("Invalid API key");
    }

    return {
      userId: client.id,
      clientId: crypto.randomUUID(),
      channels: client.allowedChannels
    };
  },

  onConnect(ctx) {
    console.log(`API client ${ctx.meta.userId} connected`);
  }
});

Related Documentation