Back to Rivet

Authentication

website/src/content/docs/actors/authentication.mdx

2.2.115.8 KB
Original Source

Do You Need Authentication?

<Tabs> <Tab title="Rivet Cloud"> Actors are private by default on Rivet Cloud. Only requests with the publishable token can interact with actors.
- **Backend-only actors**: If your publishable token is only included in your backend, then authentication is not necessary.
- **Frontend-accessible actors**: If your publishable token is included in your frontend, then implementing authentication is recommended.
</Tab> <Tab title="Self-Hosted"> Actors are public by default on self-hosted Rivet. Anyone can access them without a token.
- **Only accessible within private network**: If Rivet is only accessible within your private network, then authentication is not necessary.
- **Rivet exposed to the public internet**: If Rivet is configured to accept traffic from the public internet, then implementing authentication is recommended.
</Tab> </Tabs>

Authentication Connections

Authentication is configured through either:

  • onBeforeConnect for simple pass/fail validation
  • createConnState when you need to access user data in your actions via c.conn.state

Access Control

After a connection is authenticated, use Access Control to enforce authorization:

  • Check permissions in action handlers.
  • Use queues.<name>.canPublish to gate inbound queue publishes.
  • Use events.<name>.canSubscribe to gate event subscriptions.

onBeforeConnect

The onBeforeConnect hook validates credentials before allowing a connection. Throw an error to reject the connection.

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  authToken: string;
}

// Example token validation function
async function validateToken(token: string, roomKey: string[]): Promise<boolean> {
  // In production, verify JWT or call auth service
  return token.length > 0 && roomKey.length > 0;
}

interface Message {
  text: string;
  timestamp: number;
}

const chatRoom = actor({
  state: { messages: [] as Message[] },

  onBeforeConnect: async (c, params: ConnParams) => {
    const roomName = c.key;
    const isValid = await validateToken(params.authToken, roomName);
    if (!isValid) {
      throw new UserError("Forbidden", { code: "forbidden" });
    }
  },

  actions: {
    sendMessage: (c, text: string) => {
      c.state.messages.push({ text, timestamp: Date.now() });
    },
  },
});

createConnState

Use createConnState to extract user data from credentials and store it in connection state. This data is accessible in actions via c.conn.state. Like onBeforeConnect, throwing an error will reject the connection. See connections for more details.

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  authToken: string;
}

interface ConnState {
  userId: string;
  role: string;
}

interface Message {
  userId: string;
  text: string;
  timestamp: number;
}

// Example token validation function
async function validateToken(token: string, roomKey: string[]): Promise<{ sub: string; role: string } | null> {
  // In production, verify JWT or call auth service
  if (token.length > 0 && roomKey.length > 0) {
    return { sub: "user-123", role: "member" };
  }
  return null;
}

const chatRoom = actor({
  state: { messages: [] as Message[] },

  createConnState: async (c, params: ConnParams): Promise<ConnState> => {
    const roomName = c.key;
    const payload = await validateToken(params.authToken, roomName);
    if (!payload) {
      throw new UserError("Forbidden", { code: "forbidden" });
    }
    return {
      userId: payload.sub,
      role: payload.role,
    };
  },

  actions: {
    sendMessage: (c, text: string) => {
      // Access user data via c.conn.state
      const { userId, role } = c.conn.state;

      if (role !== "member") {
        throw new UserError("Insufficient permissions", { code: "insufficient_permissions" });
      }

      c.state.messages.push({ userId, text, timestamp: Date.now() });
      c.broadcast("newMessage", { userId, text });
    },
  },
});

Available Auth Data

Authentication hooks have access to several properties:

PropertyDescription
paramsCustom data passed by the client when connecting (see connection params)
c.requestThe underlying HTTP request object
c.request.headersRequest headers for tokens, API keys (does not work for .connect())
c.stateActor state for authorization decisions (see state)
c.keyThe actor's key (see keys)

It's recommended to use params instead of c.request.headers whenever possible since it works for both HTTP & WebSocket connections.

Client Usage

Passing Credentials

Pass authentication data when connecting. Use getParams when you need a fresh JWT for every connection or reconnect:

<CodeGroup> ```typescript Connection import { createClient } from "rivetkit/client";

async function getAuthToken(): Promise<string> { return "jwt-token-here"; }

const client = createClient(); const chat = client.chatRoom.getOrCreate(["general"], { getParams: async () => ({ authToken: await getAuthToken(), }), });

// Authentication will happen on connect by reading connection parameters const connection = chat.connect();


```typescript Stateless-Action
import { createClient } from "rivetkit/client";

const client = createClient();
const chat = client.chatRoom.getOrCreate(["general"], {
  params: { authToken: "jwt-token-here" },
});

// Authentication will happen when calling the action by reading input
// parameters
await chat.sendMessage("Hello, world!");
typescript
import { createClient } from "rivetkit/client";

// This only works for stateless actions, not WebSockets
const client = createClient({
  headers: {
    Authorization: "Bearer my-token",
  },
});

const chat = client.chatRoom.getOrCreate(["general"]);

// Authentication will happen when calling the action by reading headers
await chat.sendMessage("Hello, world!");
</CodeGroup>

Handling Errors

Authentication errors use the same system as regular errors. See errors for more details.

<CodeGroup> ```typescript Connection import { actor, setup } from "rivetkit"; import { ActorError, createClient } from "rivetkit/client";

// Define actor with protected action const myActor = actor({ state: {}, actions: { protectedAction: (c) => ({ success: true }) } });

const registry = setup({ use: { myActor } }); const client = createClient<typeof registry>("http://localhost:6420"); const actorHandle = await client.myActor.getOrCreate();

// Helper to show errors function showError(message: string) { console.error(message); }

const conn = actorHandle.connect(); conn.on("error", (error: ActorError) => { if (error.code === "forbidden") { window.location.href = "/login"; } else if (error.code === "insufficient_permissions") { showError("You don't have permission for this action"); } });


```typescript Stateless-Action
import { actor, setup } from "rivetkit";
import { ActorError, createClient } from "rivetkit/client";

// Define actor with protected action
const myActor = actor({
  state: {},
  actions: {
    protectedAction: (c) => ({ success: true })
  }
});

const registry = setup({ use: { myActor } });
const client = createClient<typeof registry>("http://localhost:6420");
const actorHandle = await client.myActor.getOrCreate();

// Helper to show errors
function showError(message: string) {
  console.error(message);
}

try {
  const result = await actorHandle.protectedAction();
} catch (error) {
  if (error instanceof ActorError && error.code === "forbidden") {
    window.location.href = "/login";
  } else if (error instanceof ActorError && error.code === "insufficient_permissions") {
    showError("You don't have permission for this action");
  }
}
</CodeGroup>

Examples

JWT

Validate JSON Web Tokens and extract user claims:

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  token: string;
}

interface ConnState {
  userId: string;
  role: string;
  permissions: string[];
}

interface JwtPayload {
  sub: string;
  role: string;
  permissions?: string[];
}

// Example JWT verification function - in production use a JWT library
function verifyJwt(token: string, secret: string): JwtPayload {
  // This is a simplified example - use jsonwebtoken or similar in production
  const parts = token.split(".");
  if (parts.length !== 3) throw new Error("Invalid token");
  const payload = JSON.parse(atob(parts[1])) as JwtPayload;
  return payload;
}

const jwtActor = actor({
  state: {},

  createConnState: (c, params: ConnParams): ConnState => {
    try {
      const payload = verifyJwt(params.token, process.env.JWT_SECRET || "secret");
      return {
        userId: payload.sub,
        role: payload.role,
        permissions: payload.permissions || [],
      };
    } catch {
      throw new UserError("Invalid or expired token", { code: "invalid_token" });
    }
  },

  actions: {
    protectedAction: (c) => {
      if (!c.conn.state.permissions.includes("write")) {
        throw new UserError("Write permission required", { code: "forbidden" });
      }
      return { success: true };
    },
  },
});

External Auth Provider

Validate credentials against an external authentication service:

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  apiKey: string;
}

interface ConnState {
  userId: string;
  tier: string;
}

const apiActor = actor({
  state: {},

  createConnState: async (c, params: ConnParams): Promise<ConnState> => {
    const response = await fetch(`https://api.my-auth-provider.com/validate`, {
      method: "POST",
      headers: { "X-API-Key": params.apiKey },
    });

    if (!response.ok) {
      throw new UserError("Invalid API key", { code: "invalid_api_key" });
    }

    const data = await response.json();
    return { userId: data.id, tier: data.tier };
  },

  actions: {
    premiumAction: (c) => {
      if (c.conn.state.tier !== "premium") {
        throw new UserError("Premium subscription required", { code: "forbidden" });
      }
      return "Premium content";
    },
  },
});

Using c.state In Authorization

Access actor state via c.state and the actor's key via c.key to make authorization decisions:

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  userId?: string;
}

const userProfile = actor({
  state: {
    ownerId: "user-123",
    isPrivate: true,
  },

  onBeforeConnect: (c, params: ConnParams) => {
    // Use actor state to check access permissions
    if (c.state.isPrivate && params.userId !== c.state.ownerId) {
      throw new UserError("Access denied to private profile", { code: "forbidden" });
    }
  },

  actions: {
    getProfile: (c) => ({ ownerId: c.state.ownerId }),
  },
});

Role-Based Access Control

Create helper functions for common authorization patterns:

typescript
import { actor, UserError } from "rivetkit";

const ROLE_HIERARCHY = { user: 1, moderator: 2, admin: 3 };

interface ConnState {
  role: keyof typeof ROLE_HIERARCHY;
  permissions: string[];
}

// Example token validation function
async function validateToken(token: string): Promise<{ role: keyof typeof ROLE_HIERARCHY; permissions: string[] }> {
  // In production, verify JWT or call auth service
  return { role: "user", permissions: ["read", "edit_posts"] };
}

function requireRole(requiredRole: keyof typeof ROLE_HIERARCHY) {
  return (c: { conn: { state: ConnState } }) => {
    const userRole = c.conn.state.role;
    if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[requiredRole]) {
      throw new UserError(`${requiredRole} role required`, { code: "forbidden" });
    }
  };
}

function requirePermission(permission: string) {
  return (c: { conn: { state: ConnState } }) => {
    if (!c.conn.state.permissions?.includes(permission)) {
      throw new UserError(`Permission '${permission}' required`, { code: "forbidden" });
    }
  };
}

const forumActor = actor({
  state: {},

  createConnState: async (c, params: { token: string }): Promise<ConnState> => {
    const user = await validateToken(params.token);
    return { role: user.role, permissions: user.permissions };
  },

  actions: {
    deletePost: (c, postId: string) => {
      requireRole("moderator")(c);
      // Delete post...
    },

    editPost: (c, postId: string, content: string) => {
      requirePermission("edit_posts")(c);
      // Edit post...
    },
  },
});

Rate Limiting

Use c.vars to track connection attempts and rate limit by user:

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  authToken: string;
}

interface RateLimitEntry {
  count: number;
  resetAt: number;
}

// Example token validation function
async function validateToken(token: string): Promise<{ userId: string }> {
  // In production, verify JWT or call auth service
  return { userId: "user-123" };
}

const rateLimitedActor = actor({
  state: {},
  createVars: () => ({ rateLimits: {} as Record<string, RateLimitEntry> }),

  onBeforeConnect: async (c, params: ConnParams) => {
    // Extract user ID
    const { userId } = await validateToken(params.authToken);

    // Check rate limit
    const now = Date.now();
    const limit = c.vars.rateLimits[userId];

    if (limit && limit.resetAt > now && limit.count >= 10) {
      throw new UserError("Too many requests, try again later", { code: "rate_limited" });
    }

    // Update rate limit
    if (!limit || limit.resetAt <= now) {
      c.vars.rateLimits[userId] = { count: 1, resetAt: now + 60_000 };
    } else {
      limit.count++;
    }
  },

  actions: {
    getData: (c) => ({ success: true }),
  },
});

The limits in this example are ephemeral. If you wish to persist rate limits, you can optionally replace vars with state.

Caching Tokens

Cache validated tokens in c.vars to avoid redundant validation on repeated connections. See ephemeral variables for more details.

typescript
import { actor, UserError } from "rivetkit";

interface ConnParams {
  authToken: string;
}

interface ConnState {
  userId: string;
  role: string;
}

interface TokenCache {
  [token: string]: {
    userId: string;
    role: string;
    expiresAt: number;
  };
}

// Example token validation function
async function validateToken(token: string): Promise<{ sub: string; role: string } | null> {
  // In production, verify JWT or call auth service
  if (token.length > 0) {
    return { sub: "user-123", role: "member" };
  }
  return null;
}

const cachedAuthActor = actor({
  state: {},
  createVars: () => ({ tokenCache: {} as TokenCache }),

  createConnState: async (c, params: ConnParams): Promise<ConnState> => {
    const token = params.authToken;

    // Check cache first
    const cached = c.vars.tokenCache[token];
    if (cached && cached.expiresAt > Date.now()) {
      return { userId: cached.userId, role: cached.role };
    }

    // Validate token (expensive operation)
    const payload = await validateToken(token);
    if (!payload) {
      throw new UserError("Invalid token", { code: "invalid_token" });
    }

    // Cache the result
    c.vars.tokenCache[token] = {
      userId: payload.sub,
      role: payload.role,
      expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
    };

    return { userId: payload.sub, role: payload.role };
  },

  actions: {
    getData: (c) => ({ userId: c.conn.state.userId }),
  },
});

API Reference