Skip to content

Call an authenticated API from a sandbox

Last updated View as MarkdownAgent setup

Let code in a sandbox call an API that needs a token, while the token stays in your Worker. Requests from the sandbox reach an entrypoint in your Worker, which allows only the requests you choose and adds the token. In this example, the code reads your profile from the GitHub API.

Prerequisites

Store the token

  1. In wrangler.jsonc, declare the token as a secret, then generate types:

    {
    	"secrets": {
    		"required": ["GITHUB_TOKEN"],
    	},
    }
    [secrets]
    required = [ "GITHUB_TOKEN" ]
    npx wrangler types
  2. Store the token when Wrangler prompts for it:

    npx wrangler secret put GITHUB_TOKEN

Call the API from a Container

The container sends its request to api.github.com as usual. The Durable Object intercepts HTTPS requests to that hostname and delivers them to an entrypoint in your Worker, which adds the token.

  1. Add an entrypoint to your Worker that accepts one GitHub API request and adds the token:

    src/index.jsjs
    import { WorkerEntrypoint } from "cloudflare:workers";
    
    export class GitHubGateway extends WorkerEntrypoint {
    	async fetch(request) {
    		const url = new URL(request.url);
    
    		if (
    			request.method !== "GET" ||
    			url.hostname !== "api.github.com" ||
    			url.pathname !== "/user" ||
    			url.search !== ""
    		) {
    			return new Response("Forbidden", { status: 403 });
    		}
    
    		return fetch("https://api.github.com/user", {
    			headers: {
    				Accept: "application/vnd.github+json",
    				Authorization: `Bearer ${this.env.GITHUB_TOKEN}`,
    				"User-Agent": "cloudflare-sandbox",
    				"X-GitHub-Api-Version": "2022-11-28",
    			},
    		});
    	}
    }
    src/index.tsts
    import { WorkerEntrypoint } from "cloudflare:workers";
    
    export class GitHubGateway extends WorkerEntrypoint<Env> {
    	async fetch(request: Request): Promise<Response> {
    		const url = new URL(request.url);
    
    		if (
    			request.method !== "GET" ||
    			url.hostname !== "api.github.com" ||
    			url.pathname !== "/user" ||
    			url.search !== ""
    		) {
    			return new Response("Forbidden", { status: 403 });
    		}
    
    		return fetch("https://api.github.com/user", {
    			headers: {
    				Accept: "application/vnd.github+json",
    				Authorization: `Bearer ${this.env.GITHUB_TOKEN}`,
    				"User-Agent": "cloudflare-sandbox",
    				"X-GitHub-Api-Version": "2022-11-28",
    			},
    		});
    	}
    }

    The entrypoint allows only GET /user, so code in the sandbox cannot use the token for anything else. Allow each request your code needs, and nothing more.

  2. Add a method to your Durable Object that intercepts api.github.com and runs code that calls it:

    src/index.tsts
    export class MyContainer extends DurableObject<Env> {
    	// ...
    
    	async getUsername(): Promise<string> {
    		const container = this.ctx.container;
    
    		if (!container) {
    			throw new Error("The container binding is not configured");
    		}
    
    		await container.interceptOutboundHttps(
    			"api.github.com",
    			this.ctx.exports.GitHubGateway,
    		);
    
    		if (!container.running) {
    			container.start({
    				image: "cloudflare/debian-trixie",
    				entrypoint: ["sleep", "infinity"],
    				// The container can reach only the hostnames you intercept
    				enableInternet: false,
    			});
    		}
    
    		const process = await container.exec(
    			[
    				"node",
    				"--input-type=module",
    				"--eval",
    				`const response = await fetch("https://api.github.com/user");
    if (!response.ok) throw new Error("GitHub request failed");
    console.log((await response.json()).login);`,
    			],
    			{
    				env: {
    					// The intercept terminates TLS with a certificate that the container
    					// CA certificate signs. Node.js reads the CA certificate from this path
    					NODE_EXTRA_CA_CERTS: "/etc/cloudflare/certs/cloudflare-containers-ca.crt",
    				},
    			},
    		);
    		const output = await process.output();
    
    		if (output.exitCode !== 0) {
    			throw new Error(new TextDecoder().decode(output.stderr));
    		}
    
    		return new TextDecoder().decode(output.stdout).trim();
    	}
    }
  3. Add a route to your Worker that returns the username:

    src/index.tsts
    if (url.pathname === "/username") {
    	const sandbox = env.MY_CONTAINER.getByName("sandbox");
    	return Response.json({ username: await sandbox.getUsername() });
    }
  4. Deploy your Worker, then send a request to /username on the workers.dev URL that Wrangler prints:

    npx wrangler deploy
    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/username
    { "username": "octocat" }

Call the API from a Dynamic Worker

A Dynamic Worker gets a method that calls one fixed GitHub endpoint, instead of the token.

  1. Add an entrypoint to your Worker with a method that calls GitHub with the token:

    src/index.jsjs
    import { WorkerEntrypoint } from "cloudflare:workers";
    
    export class GitHub extends WorkerEntrypoint {
    	async getUsername() {
    		const response = await fetch("https://api.github.com/user", {
    			headers: {
    				Accept: "application/vnd.github+json",
    				Authorization: `Bearer ${this.env.GITHUB_TOKEN}`,
    				"User-Agent": "cloudflare-sandbox",
    				"X-GitHub-Api-Version": "2022-11-28",
    			},
    		});
    
    		if (!response.ok) {
    			throw new Error(`GitHub returned ${response.status}`);
    		}
    
    		const user = await response.json();
    		return user.login;
    	}
    }
    src/index.tsts
    import { WorkerEntrypoint } from "cloudflare:workers";
    
    export class GitHub extends WorkerEntrypoint<Env> {
    	async getUsername(): Promise<string> {
    		const response = await fetch("https://api.github.com/user", {
    			headers: {
    				Accept: "application/vnd.github+json",
    				Authorization: `Bearer ${this.env.GITHUB_TOKEN}`,
    				"User-Agent": "cloudflare-sandbox",
    				"X-GitHub-Api-Version": "2022-11-28",
    			},
    		});
    
    		if (!response.ok) {
    			throw new Error(`GitHub returned ${response.status}`);
    		}
    
    		const user = await response.json<{ login: string }>();
    		return user.login;
    	}
    }
  2. In the fetch() handler of your Worker, pass the entrypoint to the Dynamic Worker as a binding. The handler needs its ctx argument for ctx.exports:

    src/index.tsts
    const sandbox = env.LOADER.load({
    	compatibilityDate: "$today",
    	mainModule: "code.js",
    	modules: {
    		"code.js": `
    			import { WorkerEntrypoint } from "cloudflare:workers";
    
    			export class Code extends WorkerEntrypoint {
    				async run() {
    					return this.env.GITHUB.getUsername();
    				}
    			}
    		`,
    	},
    	env: {
    		// Create a stub that the Dynamic Worker can receive
    		GITHUB: ctx.exports.GitHub({ props: {} }),
    	},
    	// Block every other `fetch()` and `connect()` call from the Dynamic Worker
    	globalOutbound: null,
    });
    
    const username = await sandbox
    	.getEntrypoint<WorkerEntrypoint & { run(): Promise<string> }>("Code")
    	.run();
    return Response.json({ username });

    The code calls this.env.GITHUB.getUsername(), and the method reads the token in your Worker.

  3. Deploy your Worker, then send a request to the workers.dev URL that Wrangler prints:

    npx wrangler deploy
    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev
    { "username": "octocat" }

Authenticate callers to your Worker first, so other people cannot use the quota of your token. For more information, refer to Sandbox security.

Was this helpful?