# Authentication

## Do You Need Authentication?

### 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.

### 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.

## 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](/actors/docs/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.

examples/docs/actors-authentication/on-before-connect.ts:

```ts
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](/actors/docs/connections) for more details.

examples/docs/actors-authentication/create-conn-state.ts:

```ts
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:

| Property | Description |
|----------|-------------|
| `params` | Custom data passed by the client when connecting (see [connection params](/actors/docs/connections#extracting-data-from-connection-params)) |
| `c.request` | The underlying HTTP request object |
| `c.request.headers` | Request headers for tokens, API keys (does not work for `.connect()`) |
| `c.state` | Actor state for authorization decisions (see [state](/actors/docs/state)) |
| `c.key` | The actor's key (see [keys](/actors/docs/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:

Connection:

```ts
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();
```

Stateless-Action:

```ts
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!");
```

HTTP-Headers:

```ts
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!");
```

### Handling Errors

Authentication errors use the same system as regular errors. See [errors](/actors/docs/errors) for more details.

Connection:

```ts
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.onError((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");
  }
});
```

Stateless-Action:

```ts
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");
  }
}
```

## Examples

### JWT

Validate JSON Web Tokens and extract user claims:

examples/docs/actors-authentication/jwt.ts:

```ts
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:

examples/docs/actors-authentication/external-auth-provider.ts:

```ts
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:

examples/docs/actors-authentication/using-state.ts:

```ts
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:

examples/docs/actors-authentication/role-based-access-control.ts:

```ts
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:

examples/docs/actors-authentication/rate-limiting.ts:

```ts
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](/actors/docs/state#ephemeral-variables). 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](/actors/docs/state#ephemeral-variables) for more details.

examples/docs/actors-authentication/caching-tokens.ts:

```ts
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 }),
  },
});
```
