Skip to content

Authentication

Last updated View as MarkdownAgent setup

Anyone who knows an agent's URL can send it requests. Check the caller in your Worker, before the request reaches the Durable Object. The object then only sees requests you have already allowed.

Use the routing hooks

routeAgentRequest() runs onBeforeConnect before it forwards a WebSocket upgrade, and onBeforeRequest before it forwards an HTTP request. Both run in the Worker. Each hook can:

  • Return a Response to reject the request.
  • Return a new Request to replace it, for example to add a header the object trusts.
  • Return nothing to forward the request unchanged.

The route argument holds the matched binding name as className, and the object's name:

src/index.jsjs
import { routeAgentRequest } from "agents/routing";

async function authenticate(request, env) {
	// Verify a session cookie, a JWT, or an API key here.
	const token = request.headers.get("Authorization");
	return token ? { id: "user-123" } : null;
}

export default {
	async fetch(request, env) {
		const check = async (req, route) => {
			const user = await authenticate(req, env);
			if (!user) {
				return new Response("Unauthorized", { status: 401 });
			}
			// Each user can only reach the agent named after them.
			if (route.name !== user.id) {
				return new Response("Forbidden", { status: 403 });
			}
			const forwarded = new Request(req);
			forwarded.headers.set("x-user-id", user.id);
			return forwarded;
		};

		return (
			(await routeAgentRequest(request, env, {
				onBeforeConnect: check,
				onBeforeRequest: check,
			})) ?? new Response("Not found", { status: 404 })
		);
	},
};
src/index.tsts
import { routeAgentRequest } from "agents/routing";

type User = { id: string };

async function authenticate(request: Request, env: Env): Promise<User | null> {
	// Verify a session cookie, a JWT, or an API key here.
	const token = request.headers.get("Authorization");
	return token ? { id: "user-123" } : null;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const check = async (req: Request, route: { name: string }) => {
			const user = await authenticate(req, env);
			if (!user) {
				return new Response("Unauthorized", { status: 401 });
			}
			// Each user can only reach the agent named after them.
			if (route.name !== user.id) {
				return new Response("Forbidden", { status: 403 });
			}
			const forwarded = new Request(req);
			forwarded.headers.set("x-user-id", user.id);
			return forwarded;
		};

		return (
			(await routeAgentRequest(request, env, {
				onBeforeConnect: check,
				onBeforeRequest: check,
			})) ?? new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Browsers cannot set an Authorization header on a WebSocket upgrade. For WebSocket clients, read a token from a cookie or a query parameter in onBeforeConnect. useAgent and AgentClient accept a query option for this.

A header you add in the hook is only trustworthy if clients cannot reach the object any other way. Route every request to the object through the same check.

Check before routing

You can also check the request before you call routeAgentRequest(). This works for any route, including ones routeAgentRequest() does not handle:

src/index.jsjs
import { routeAgentRequest } from "agents/routing";

async function authenticate(request) {
	const token = request.headers.get("Authorization");
	return token ? { id: "user-123" } : null;
}

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (url.pathname.startsWith("/agents/")) {
			const user = await authenticate(request);
			if (!user) {
				return new Response("Unauthorized", { status: 401 });
			}
			// /agents/:binding/:name — the name is the fourth segment.
			const name = url.pathname.split("/")[3];
			if (name !== user.id) {
				return new Response("Forbidden", { status: 403 });
			}
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
src/index.tsts
import { routeAgentRequest } from "agents/routing";

async function authenticate(request: Request): Promise<{ id: string } | null> {
	const token = request.headers.get("Authorization");
	return token ? { id: "user-123" } : null;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname.startsWith("/agents/")) {
			const user = await authenticate(request);
			if (!user) {
				return new Response("Unauthorized", { status: 401 });
			}
			// /agents/:binding/:name — the name is the fourth segment.
			const name = url.pathname.split("/")[3];
			if (name !== user.id) {
				return new Response("Forbidden", { status: 403 });
			}
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Pick the name on the server

Instead of checking the name the client sent, you can choose it yourself from the session. The client then never names the object. For more information, refer to Custom routing.

Restrict what a connection can do

Authentication decides who reaches the object. Inside the object, the WebSockets capability can still limit each connection. For example, make viewers' connections readonly, so they receive state but cannot change it.

Was this helpful?