From 1c285303d618e209f08889aae823b23b86ed5bd2 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 10:09:18 -0700 Subject: [PATCH 01/10] [Workers] Document auth patterns for generated apps --- .../configuration/bindings.mdx | 2 + .../configuration/dynamic-dispatch.mdx | 2 + src/content/docs/dynamic-workers/index.mdx | 1 + .../docs/dynamic-workers/usage/bindings.mdx | 2 + .../authenticate-generated-apps.mdx | 328 ++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 src/content/docs/workers/best-practices/authenticate-generated-apps.mdx diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx index 9c115608604..f1353ab0298 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx @@ -23,6 +23,8 @@ With bindings, each User Worker can extend functionality to: - **Connect to private networks** with [VPC Services](/workers-vpc/configuration/vpc-services/), [VPC Networks](/workers-vpc/configuration/vpc-networks/), and [Hyperdrive](/hyperdrive/) - **Collect metrics** with [Analytics Engine](/analytics/analytics-engine/) +For an example that checks both tenant and record ownership, refer to [Authorize the protected operation](/workers/best-practices/authenticate-generated-apps/#authorize-the-protected-operation). + #### Resource isolation Each User Worker can only access the bindings that are explicitly attached to it. For complete isolation, you can create and attach a unique resource (like a D1 database or KV namespace) to every User Worker. diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx index ddc9334f525..b6351b771bb 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx @@ -18,6 +18,8 @@ A [dynamic dispatch Worker](/cloudflare-for-platforms/workers-for-platforms/how- You can also create a dispatch Worker from the Cloudflare dashboard. Go to **Workers for Platforms**, select your namespace, and click **Create** > **Dispatch Worker**. The dashboard provides templates for path-based and subdomain-based routing. ::: +For apps with public and private routes, refer to [Authenticate and authorize generated apps](/workers/best-practices/authenticate-generated-apps/). The guide combines route policies with scoped data access. + #### Why use a dynamic dispatch Worker? - **Scale**: Route requests to millions of hostnames to different Workers, without defining [Workers Routes](/workers/configuration/routing/routes/) configuration for each one diff --git a/src/content/docs/dynamic-workers/index.mdx b/src/content/docs/dynamic-workers/index.mdx index d4cc569b9fa..71a870fc877 100644 --- a/src/content/docs/dynamic-workers/index.mdx +++ b/src/content/docs/dynamic-workers/index.mdx @@ -40,6 +40,7 @@ Use Dynamic Workers when code needs to run quickly in a secure, isolated environ Because you compose the Worker that runs the code at runtime, you control how that Worker is configured and what it can access. +- **[Authentication and authorization](/workers/best-practices/authenticate-generated-apps/)**: Enforce route policies and protect data access outside generated code. - **[Bindings](/dynamic-workers/usage/bindings/)**: Decide which bindings and structured data the dynamic Worker receives. - **[Observability](/dynamic-workers/usage/observability/)**: Attach Tail Workers and capture logs for each run. - **[Network access](/dynamic-workers/usage/egress-control/)**: Intercept or block Internet access for outbound requests. diff --git a/src/content/docs/dynamic-workers/usage/bindings.mdx b/src/content/docs/dynamic-workers/usage/bindings.mdx index 62357ba21ab..4c7467f4c55 100644 --- a/src/content/docs/dynamic-workers/usage/bindings.mdx +++ b/src/content/docs/dynamic-workers/usage/bindings.mdx @@ -18,6 +18,8 @@ This allows you to: - **Expose custom capabilities**: Define your own methods that Dynamic Workers can call, such as posting to a chat room, sending an email, or querying an internal service. You design the interface, and the Dynamic Worker calls it. - **Restrict and control access**: Inspect, transform, or reject calls before they reach the underlying resource. +For apps with public and private routes, refer to [Authenticate and authorize generated apps](/workers/best-practices/authenticate-generated-apps/). The guide checks requests before invocation and permissions inside platform-owned operations. + ## Custom bindings with Dynamic Workers With custom bindings, you: diff --git a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx new file mode 100644 index 00000000000..03105a44a53 --- /dev/null +++ b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx @@ -0,0 +1,328 @@ +--- +title: Authenticate and authorize generated apps +description: Enforce route policies before generated code runs, and protect data through scoped capabilities on Workers for Platforms and Dynamic Workers. +pcx_content_type: design-guide +sidebar: + order: 2 +products: + - workers + - cloudflare-for-platforms + - dynamic-workers +--- + +import { Tabs, TabItem, TypeScriptExample } from "~/components"; + +Generated apps can add routes without adding authentication checks. Enforce access in platform-owned code, before invoking the app. + +For apps with public routes, also authorize sensitive data operations. A public route must not grant unrestricted database access. + +```mermaid +flowchart TB + Request --> Gate[Platform Worker
Authenticate and check route] + Gate -->|Denied| Reject[Return 401, 403, or 404
App does not run] + Gate -->|Allowed| App[Generated app] + App --> Capability[Platform capability
Check access to this record] + Capability --> Data[(Private data)] +``` + +Authentication establishes identity. Authorization determines which routes, actions, and records that identity can access. + +## Choose where to enforce access + +Choose checks according to what needs protection: + +| Requirement | Enforcement point | +| -------------------------------------------------- | -------------------------------------------- | +| The whole app requires sign-in | Dispatch or loader Worker, before invocation | +| Routes mix public and private access | Platform-owned method/path policy | +| Routes are unknown, but data permissions are known | Platform-owned data operations | +| Routing and policy must stay together | Registered handlers with access metadata | +| Public code needs fewer privileges | Separate Workers with different capabilities | +| App libraries call HTTP APIs | Outbound Worker or `globalOutbound` gateway | + +Combine route checks with data-operation checks for mixed-access apps. + +## Publish code and route policy together + +Your platform does not need to predict every future route. Require each app to declare its routes when it publishes: + +```json title="routes.json" +[ + { "method": "GET", "path": "/", "access": "public" }, + { "method": "GET", "path": "/profile", "access": "member" }, + { "method": "GET", "path": "/admin", "access": "reports:admin" } +] +``` + +This manifest is a policy format your platform implements. Workers does not infer intended access from generated code. + +```mermaid +flowchart LR + Upload[Customer submits code + policy] --> Validate[Platform validates policy] + Validate --> Release[Version code + policy together] + Release --> Publish[Publish the matching release] +``` + +The platform owns these publication rules: + +- Match the tenant to the authenticated app owner. +- Reject duplicate or ambiguous routes. +- Restrict which routes can become public. +- Prevent changes that weaken required permissions. +- Keep code and policy versions consistent during rollouts. + +For Workers for Platforms, upload a new script name first. Then switch the dispatch Worker to that script and its matching policy. + +### Deny undeclared routes + +Match both method and path. A new route stays inaccessible until the platform accepts its policy. + + +```ts +export type Identity = { + userId: string; + tenant: string; + permissions: string[]; +}; + +export type Route = { + method: string; + path: string; + access: "public" | "member" | "reports:admin"; +}; + +export function checkRoute( + request: Request, + routes: Route[], + tenant: string, + identity: Identity | null, +): Response | null { + const path = new URL(request.url).pathname; + const route = routes.find( + (route) => route.method === request.method && route.path === path, + ); + if (!route) return new Response("Route not published", { status: 404 }); + if (route.access === "public") return null; + if (!identity) return new Response("Sign in", { status: 401 }); + if ( + identity.tenant !== tenant || + (route.access !== "member" && !identity.permissions.includes(route.access)) + ) { + return new Response("Forbidden", { status: 403 }); + } + return null; +} +``` + + +Here, `member` means membership in the app tenant. Add an app-membership check if only some tenant members may enter. + +Use one path interpretation across the gate and app. This example matches exact URL paths; it grants no wildcard access. + +## Authenticate before invoking the app + +Implement `authenticate(request)` in `auth.ts` using your existing identity provider or session service. This integration must: + +- Verify the session or token, including expiry and intended audience. +- Return a verified identity, or `null` when credentials are absent. +- Reject invalid credentials rather than trust caller-supplied identity headers. + +Resolve `deployment` from your platform's app registry. Its tenant, routes, code, and script name must not come from the generated app. + +Place this gate in the platform request handler: + + +```ts +import { checkRoute, type Identity } from "./policy"; +import { authenticate } from "./auth"; + +let identity: Identity | null; +try { + identity = await authenticate(request); +} catch { + return new Response("Invalid session", { status: 401 }); +} + +const denied = checkRoute( + request, + deployment.routes, + deployment.tenant, + identity, +); +if (denied) return denied; + +// Forward only what this GET-only example needs. Keep session credentials out. +const appRequest = new Request(request.url, { method: "GET" }); +``` + + + +Invoke the app only after the gate succeeds: + + + + +Use the [dispatch namespace binding](/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/): + + +```ts +const app = env.DISPATCHER.get(deployment.scriptName, { + props: { identity }, +}); +return app.fetch(appRequest); +``` + + +All app ingress must pass through the dispatch Worker. + + + + +Authenticate in the parent Worker before calling the [Worker Loader](/dynamic-workers/api-reference/): + + +```ts +const app = env.LOADER.load({ + compatibilityDate: "$today", + mainModule: "app.js", + modules: { "app.js": deployment.code }, + globalOutbound: null, +}); +return app.getEntrypoint(undefined, { props: { identity } }).fetch(appRequest); +``` + + +`load()` creates a fresh Worker. With `get()`, keep code and configuration identical for each cache ID; do not cache one visitor's credentials for other visitors. + + + + +For a wholly private app, require membership on every request. For mixed-access apps, use the route policy to select public exceptions. + +## Authorize the protected operation + +A generated app can omit `checkAuth()` or ignore its result. Give it an operation that performs authorization and data access together. + +```mermaid +sequenceDiagram + participant Caller as Bob + participant Platform + participant App as Generated app + participant Reports as Platform Reports capability + Caller->>Platform: Request with Bob's session + Platform->>App: Capability scoped to Bob + App->>Reports: read("alice") + Reports-->>App: 404 — record does not belong to Bob + App-->>Caller: 404 +``` + +Keep the data and implementation in the platform Worker. This example uses synthetic records: + + +```ts +import { WorkerEntrypoint } from "cloudflare:workers"; + +const records = [ + { id: "alice", tenant: "acme", owner: "alice", text: "Alice's report" }, + { id: "bob", tenant: "acme", owner: "bob", text: "Bob's report" }, +]; + +type Scope = { identity: Identity | null; tenant: string }; + +export class Reports extends WorkerEntrypoint { + read(id: string) { + const { identity, tenant } = this.ctx.props; + if (!identity) return { status: 401, error: "Sign in" }; + if ( + identity.tenant !== tenant || + !identity.permissions.includes("reports:read") + ) { + return { status: 403, error: "Forbidden" }; + } + const report = records.find( + (record) => + record.id === id && + record.tenant === tenant && + record.owner === identity.userId, + ); + if (!report) return { status: 404, error: "Report not found" }; + return { status: 200, report: { text: report.text } }; + } +} +``` + + +Create the capability using verified platform context. The app can choose a record ID, but cannot choose the capability's identity: + + +```ts +const reports = ctx.exports.Reports({ + props: { identity, tenant: deployment.tenant }, +}); +``` + + +Pass the capability with the invocation: + + + + +```ts +const app = env.DISPATCHER.get(deployment.scriptName, { + props: { reports }, +}); +return app.fetch(appRequest); +``` + + + + +```ts +return app.getEntrypoint(undefined, { props: { reports } }).fetch(appRequest); +``` + + + + +The generated app calls the capability through [RPC](/workers/runtime-apis/rpc/): + + +```ts +const result = await this.ctx.props.reports.read(reportId); +return Response.json(result, { status: result.status }); +``` + + +Do not also give the app unrestricted database access. [Upload-time bindings](/cloudflare-for-platforms/workers-for-platforms/configuration/bindings/) remain available to every invocation. + +Block unrestricted network access, or mediate it through [Outbound Workers](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [Dynamic Workers egress controls](/dynamic-workers/usage/egress-control/). Otherwise, another data-access path could bypass the capability. + +## Adapt the pattern + +Use these variations when the app interface requires them: + +| Pattern | Example | Why | +| --------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| Registered handlers | `{ method: "GET", path: "/profile", access: "member", handler: "profile" }` | Select policy and handler from the same platform-owned entry. A handler name does not grant data access. | +| Public app shell | Public HTML; protected report reads through `reports.read(id)` | The platform can protect data without enumerating every frontend route. | +| Separate components | Public Worker receives `{}`; private Worker receives `{ reports }` | Public code has no private capability. Keep their storage separate too. | +| Outbound HTTP gateway | Allow `GET https://reports.internal/reports/:id`, then check record access | Existing HTTP clients can use the same authorization model. Ignore identity headers supplied by app code. | + +If the outbound gateway injects credentials, authorize the destination and operation first. Hiding a token does not limit what that token can do. + +## Verify the boundary + +Test both responses and whether generated code executed: + +| Request | Expected response | App executes | +| ---------------------------------------------------------- | -------------------- | ------------------------------------------------- | +| Anonymous request to a declared public route | `200` | Yes | +| Anonymous request to a member route | `401` | No | +| Wrong tenant or missing route permission | `403` | No | +| Undeclared route or method | `404` | No | +| Bob's public app requests Alice's record through RPC | `404` | Yes; the platform denies the read | +| App sends a forged identity header to the outbound gateway | No additional access | Yes; the gateway uses its platform-bound identity | + +Route policy cannot prove that an approved public endpoint returns only public information. Capabilities protect new operations; they cannot retract data already returned to app code. + +Per-request props do not erase a reused Worker's module state. If visitors must be isolated from data the app previously received, also isolate execution and persistent storage across those visitors. From dec9102c7e03d2e287ad3fac57431744769267b3 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 11:02:50 -0700 Subject: [PATCH 02/10] [Workers] Clarify the generated app authentication problem --- .../best-practices/authenticate-generated-apps.mdx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx index 03105a44a53..fdfc89e4685 100644 --- a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx @@ -12,9 +12,16 @@ products: import { Tabs, TabItem, TypeScriptExample } from "~/components"; -Generated apps can add routes without adding authentication checks. Enforce access in platform-owned code, before invoking the app. +Your customers can use artificial intelligence (AI) to generate apps. Each app may need both public and private endpoints. -For apps with public routes, also authorize sensitive data operations. A public route must not grant unrestricted database access. +Your platform cannot predict every route those apps will introduce. Generated code may also omit authentication or authorization checks. + +Enforce access in code your platform controls: + +- **Before the app runs:** require a list of public and private routes when the app is published. Validate that list and enforce its rules before invoking app code. +- **When the app accesses data:** check permissions inside platform-owned operations. Signing in must not let Bob read Alice's private report. + +This guide implements these checks with Workers for Platforms and Dynamic Workers. ```mermaid flowchart TB From 46cef09c3993f37b49def1922fe37cdc9c077ec6 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 11:07:19 -0700 Subject: [PATCH 03/10] [Workers] Link auth pattern choices to their documentation --- .../best-practices/authenticate-generated-apps.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx index fdfc89e4685..f203ee2e1a2 100644 --- a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx @@ -40,12 +40,12 @@ Choose checks according to what needs protection: | Requirement | Enforcement point | | -------------------------------------------------- | -------------------------------------------- | -| The whole app requires sign-in | Dispatch or loader Worker, before invocation | -| Routes mix public and private access | Platform-owned method/path policy | -| Routes are unknown, but data permissions are known | Platform-owned data operations | -| Routing and policy must stay together | Registered handlers with access metadata | -| Public code needs fewer privileges | Separate Workers with different capabilities | -| App libraries call HTTP APIs | Outbound Worker or `globalOutbound` gateway | +| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/workers/best-practices/authenticate-generated-apps/#authenticate-before-invoking-the-app) | +| Routes mix public and private access | [Platform-owned method/path policy](/workers/best-practices/authenticate-generated-apps/#publish-code-and-route-policy-together) | +| Routes are unknown, but data permissions are known | [Platform-owned data operations](/workers/best-practices/authenticate-generated-apps/#authorize-the-protected-operation) | +| Routing and policy must stay together | [Registered handlers with access metadata](/workers/best-practices/authenticate-generated-apps/#adapt-the-pattern) | +| Public code needs fewer privileges | [Separate Workers with different capabilities](/workers/best-practices/authenticate-generated-apps/#adapt-the-pattern) | +| App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | Combine route checks with data-operation checks for mixed-access apps. From f8e28d0fcfc97071fb0b8f0faa4ff873e7256640 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 11:18:09 -0700 Subject: [PATCH 04/10] [Workers] Remove the upload-time bindings paragraph --- .../docs/workers/best-practices/authenticate-generated-apps.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx index f203ee2e1a2..fbffffe87f8 100644 --- a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx @@ -300,8 +300,6 @@ return Response.json(result, { status: result.status }); ``` -Do not also give the app unrestricted database access. [Upload-time bindings](/cloudflare-for-platforms/workers-for-platforms/configuration/bindings/) remain available to every invocation. - Block unrestricted network access, or mediate it through [Outbound Workers](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [Dynamic Workers egress controls](/dynamic-workers/usage/egress-control/). Otherwise, another data-access path could bypass the capability. ## Adapt the pattern From 83d504800a59ad644f932282f02e26ce5669e717 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 11:22:31 -0700 Subject: [PATCH 05/10] [Workers] Remove the boundary verification section --- .../authenticate-generated-apps.mdx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx index fbffffe87f8..af59b3c21f4 100644 --- a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx @@ -314,20 +314,3 @@ Use these variations when the app interface requires them: | Outbound HTTP gateway | Allow `GET https://reports.internal/reports/:id`, then check record access | Existing HTTP clients can use the same authorization model. Ignore identity headers supplied by app code. | If the outbound gateway injects credentials, authorize the destination and operation first. Hiding a token does not limit what that token can do. - -## Verify the boundary - -Test both responses and whether generated code executed: - -| Request | Expected response | App executes | -| ---------------------------------------------------------- | -------------------- | ------------------------------------------------- | -| Anonymous request to a declared public route | `200` | Yes | -| Anonymous request to a member route | `401` | No | -| Wrong tenant or missing route permission | `403` | No | -| Undeclared route or method | `404` | No | -| Bob's public app requests Alice's record through RPC | `404` | Yes; the platform denies the read | -| App sends a forged identity header to the outbound gateway | No additional access | Yes; the gateway uses its platform-bound identity | - -Route policy cannot prove that an approved public endpoint returns only public information. Capabilities protect new operations; they cannot retract data already returned to app code. - -Per-request props do not erase a reused Worker's module state. If visitors must be isolated from data the app previously received, also isolate execution and persistent storage across those visitors. From f8fbd4a52322d293f1ed56a581c839568c86a6c8 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 11:28:34 -0700 Subject: [PATCH 06/10] [Workers for Platforms] Move generated-app auth guide into product docs --- public/__redirects | 1 + .../authenticate-generated-apps.mdx | 14 +++++++------- .../best-practices/index.mdx | 15 +++++++++++++++ .../configuration/bindings.mdx | 2 +- .../configuration/dynamic-dispatch.mdx | 2 +- src/content/docs/dynamic-workers/index.mdx | 2 +- .../docs/dynamic-workers/usage/bindings.mdx | 2 +- 7 files changed, 27 insertions(+), 11 deletions(-) rename src/content/docs/{workers => cloudflare-for-platforms/workers-for-platforms}/best-practices/authenticate-generated-apps.mdx (93%) create mode 100644 src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx diff --git a/public/__redirects b/public/__redirects index 56f7907c1a9..95a4a711753 100644 --- a/public/__redirects +++ b/public/__redirects @@ -1952,6 +1952,7 @@ /cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/email/ /cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/ 301 # workers for platforms +/workers/best-practices/authenticate-generated-apps/ /cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/ 301 /cloudflare-for-platforms/workers-for-platforms/learning/how-workers-for-platforms-works/ /cloudflare-for-platforms/workers-for-platforms/reference/how-workers-for-platforms-works/ 301 /cloudflare-for-platforms/workers-for-platforms/platform/observability/ /cloudflare-for-platforms/workers-for-platforms/configuration/observability/ 301 /cloudflare-for-platforms/workers-for-platforms/platform/outbound-workers/ /cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/ 301 diff --git a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx similarity index 93% rename from src/content/docs/workers/best-practices/authenticate-generated-apps.mdx rename to src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx index af59b3c21f4..badb476813b 100644 --- a/src/content/docs/workers/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx @@ -3,9 +3,9 @@ title: Authenticate and authorize generated apps description: Enforce route policies before generated code runs, and protect data through scoped capabilities on Workers for Platforms and Dynamic Workers. pcx_content_type: design-guide sidebar: - order: 2 + order: 1 + label: Authentication and authorization products: - - workers - cloudflare-for-platforms - dynamic-workers --- @@ -40,11 +40,11 @@ Choose checks according to what needs protection: | Requirement | Enforcement point | | -------------------------------------------------- | -------------------------------------------- | -| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/workers/best-practices/authenticate-generated-apps/#authenticate-before-invoking-the-app) | -| Routes mix public and private access | [Platform-owned method/path policy](/workers/best-practices/authenticate-generated-apps/#publish-code-and-route-policy-together) | -| Routes are unknown, but data permissions are known | [Platform-owned data operations](/workers/best-practices/authenticate-generated-apps/#authorize-the-protected-operation) | -| Routing and policy must stay together | [Registered handlers with access metadata](/workers/best-practices/authenticate-generated-apps/#adapt-the-pattern) | -| Public code needs fewer privileges | [Separate Workers with different capabilities](/workers/best-practices/authenticate-generated-apps/#adapt-the-pattern) | +| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authenticate-before-invoking-the-app) | +| Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#publish-code-and-route-policy-together) | +| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authorize-the-protected-operation) | +| Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#adapt-the-pattern) | +| Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#adapt-the-pattern) | | App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | Combine route checks with data-operation checks for mixed-access apps. diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx new file mode 100644 index 00000000000..04fcfeecf3f --- /dev/null +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx @@ -0,0 +1,15 @@ +--- +title: Best practices +description: Recommended patterns for authenticating requests and authorizing data access in apps built with Workers for Platforms. +pcx_content_type: navigation +sidebar: + order: 21 + group: + hideIndex: true +products: + - cloudflare-for-platforms +--- + +import { DirectoryListing } from "~/components"; + + diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx index f1353ab0298..0c659d6054e 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx @@ -23,7 +23,7 @@ With bindings, each User Worker can extend functionality to: - **Connect to private networks** with [VPC Services](/workers-vpc/configuration/vpc-services/), [VPC Networks](/workers-vpc/configuration/vpc-networks/), and [Hyperdrive](/hyperdrive/) - **Collect metrics** with [Analytics Engine](/analytics/analytics-engine/) -For an example that checks both tenant and record ownership, refer to [Authorize the protected operation](/workers/best-practices/authenticate-generated-apps/#authorize-the-protected-operation). +For an example that checks both tenant and record ownership, refer to [Authorize the protected operation](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authorize-the-protected-operation). #### Resource isolation diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx index b6351b771bb..c0c35d08263 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx @@ -18,7 +18,7 @@ A [dynamic dispatch Worker](/cloudflare-for-platforms/workers-for-platforms/how- You can also create a dispatch Worker from the Cloudflare dashboard. Go to **Workers for Platforms**, select your namespace, and click **Create** > **Dispatch Worker**. The dashboard provides templates for path-based and subdomain-based routing. ::: -For apps with public and private routes, refer to [Authenticate and authorize generated apps](/workers/best-practices/authenticate-generated-apps/). The guide combines route policies with scoped data access. +For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/). The guide combines route policies with scoped data access. #### Why use a dynamic dispatch Worker? diff --git a/src/content/docs/dynamic-workers/index.mdx b/src/content/docs/dynamic-workers/index.mdx index 71a870fc877..66a44df7682 100644 --- a/src/content/docs/dynamic-workers/index.mdx +++ b/src/content/docs/dynamic-workers/index.mdx @@ -40,7 +40,7 @@ Use Dynamic Workers when code needs to run quickly in a secure, isolated environ Because you compose the Worker that runs the code at runtime, you control how that Worker is configured and what it can access. -- **[Authentication and authorization](/workers/best-practices/authenticate-generated-apps/)**: Enforce route policies and protect data access outside generated code. +- **[Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/)**: Enforce route policies and protect data access outside generated code. - **[Bindings](/dynamic-workers/usage/bindings/)**: Decide which bindings and structured data the dynamic Worker receives. - **[Observability](/dynamic-workers/usage/observability/)**: Attach Tail Workers and capture logs for each run. - **[Network access](/dynamic-workers/usage/egress-control/)**: Intercept or block Internet access for outbound requests. diff --git a/src/content/docs/dynamic-workers/usage/bindings.mdx b/src/content/docs/dynamic-workers/usage/bindings.mdx index 4c7467f4c55..36fbf9d55ad 100644 --- a/src/content/docs/dynamic-workers/usage/bindings.mdx +++ b/src/content/docs/dynamic-workers/usage/bindings.mdx @@ -18,7 +18,7 @@ This allows you to: - **Expose custom capabilities**: Define your own methods that Dynamic Workers can call, such as posting to a chat room, sending an email, or querying an internal service. You design the interface, and the Dynamic Worker calls it. - **Restrict and control access**: Inspect, transform, or reject calls before they reach the underlying resource. -For apps with public and private routes, refer to [Authenticate and authorize generated apps](/workers/best-practices/authenticate-generated-apps/). The guide checks requests before invocation and permissions inside platform-owned operations. +For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/). The guide checks requests before invocation and permissions inside platform-owned operations. ## Custom bindings with Dynamic Workers From dd92b475c0cb0cb32bc5e7f633028d9f04b129a1 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 12:43:02 -0700 Subject: [PATCH 07/10] [Workers for Platforms] Explain why generated apps need data access checks --- .../best-practices/authenticate-generated-apps.mdx | 6 ++++-- .../workers-for-platforms/configuration/bindings.mdx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx index badb476813b..69f0dbc2d0c 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx @@ -42,7 +42,7 @@ Choose checks according to what needs protection: | -------------------------------------------------- | -------------------------------------------- | | The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authenticate-before-invoking-the-app) | | Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#publish-code-and-route-policy-together) | -| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authorize-the-protected-operation) | +| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#protect-data-even-when-generated-routes-change) | | Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#adapt-the-pattern) | | Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#adapt-the-pattern) | | App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | @@ -206,7 +206,9 @@ return app.getEntrypoint(undefined, { props: { identity } }).fetch(appRequest); For a wholly private app, require membership on every request. For mixed-access apps, use the route policy to select public exceptions. -## Authorize the protected operation +## Protect data even when generated routes change + +A route check can require Bob to sign in, but it cannot prevent app code from requesting Alice's report. Keep permission checks inside the platform operations that read private data, so every new route uses the same checks. A generated app can omit `checkAuth()` or ignore its result. Give it an operation that performs authorization and data access together. diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx index 0c659d6054e..d12f9ec922e 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx @@ -23,7 +23,7 @@ With bindings, each User Worker can extend functionality to: - **Connect to private networks** with [VPC Services](/workers-vpc/configuration/vpc-services/), [VPC Networks](/workers-vpc/configuration/vpc-networks/), and [Hyperdrive](/hyperdrive/) - **Collect metrics** with [Analytics Engine](/analytics/analytics-engine/) -For an example that checks both tenant and record ownership, refer to [Authorize the protected operation](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authorize-the-protected-operation). +For an example that checks both tenant and record ownership, refer to [Protect data even when generated routes change](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#protect-data-even-when-generated-routes-change). #### Resource isolation From 1b01c9577acd9638db9dbf1215e0a9f1ff012e25 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 12:47:50 -0700 Subject: [PATCH 08/10] [Workers for Platforms] Place authentication guide under Configuration --- public/__redirects | 2 +- .../best-practices/index.mdx | 15 --------------- .../authenticate-generated-apps.mdx | 12 ++++++------ .../configuration/bindings.mdx | 2 +- .../configuration/dynamic-dispatch.mdx | 2 +- src/content/docs/dynamic-workers/index.mdx | 2 +- .../docs/dynamic-workers/usage/bindings.mdx | 2 +- 7 files changed, 11 insertions(+), 26 deletions(-) delete mode 100644 src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx rename src/content/docs/cloudflare-for-platforms/workers-for-platforms/{best-practices => configuration}/authenticate-generated-apps.mdx (96%) diff --git a/public/__redirects b/public/__redirects index 95a4a711753..952d647bb12 100644 --- a/public/__redirects +++ b/public/__redirects @@ -1952,7 +1952,7 @@ /cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/email/ /cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/ 301 # workers for platforms -/workers/best-practices/authenticate-generated-apps/ /cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/ 301 +/workers/best-practices/authenticate-generated-apps/ /cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/ 301 /cloudflare-for-platforms/workers-for-platforms/learning/how-workers-for-platforms-works/ /cloudflare-for-platforms/workers-for-platforms/reference/how-workers-for-platforms-works/ 301 /cloudflare-for-platforms/workers-for-platforms/platform/observability/ /cloudflare-for-platforms/workers-for-platforms/configuration/observability/ 301 /cloudflare-for-platforms/workers-for-platforms/platform/outbound-workers/ /cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/ 301 diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx deleted file mode 100644 index 04fcfeecf3f..00000000000 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/index.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Best practices -description: Recommended patterns for authenticating requests and authorizing data access in apps built with Workers for Platforms. -pcx_content_type: navigation -sidebar: - order: 21 - group: - hideIndex: true -products: - - cloudflare-for-platforms ---- - -import { DirectoryListing } from "~/components"; - - diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps.mdx similarity index 96% rename from src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx rename to src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps.mdx index 69f0dbc2d0c..15a431d5c5a 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps.mdx @@ -3,7 +3,7 @@ title: Authenticate and authorize generated apps description: Enforce route policies before generated code runs, and protect data through scoped capabilities on Workers for Platforms and Dynamic Workers. pcx_content_type: design-guide sidebar: - order: 1 + order: 4 label: Authentication and authorization products: - cloudflare-for-platforms @@ -40,11 +40,11 @@ Choose checks according to what needs protection: | Requirement | Enforcement point | | -------------------------------------------------- | -------------------------------------------- | -| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#authenticate-before-invoking-the-app) | -| Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#publish-code-and-route-policy-together) | -| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#protect-data-even-when-generated-routes-change) | -| Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#adapt-the-pattern) | -| Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#adapt-the-pattern) | +| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#authenticate-before-invoking-the-app) | +| Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#publish-code-and-route-policy-together) | +| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#protect-data-even-when-generated-routes-change) | +| Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#adapt-the-pattern) | +| Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#adapt-the-pattern) | | App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | Combine route checks with data-operation checks for mixed-access apps. diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx index d12f9ec922e..f4a7c033dc4 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx @@ -23,7 +23,7 @@ With bindings, each User Worker can extend functionality to: - **Connect to private networks** with [VPC Services](/workers-vpc/configuration/vpc-services/), [VPC Networks](/workers-vpc/configuration/vpc-networks/), and [Hyperdrive](/hyperdrive/) - **Collect metrics** with [Analytics Engine](/analytics/analytics-engine/) -For an example that checks both tenant and record ownership, refer to [Protect data even when generated routes change](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/#protect-data-even-when-generated-routes-change). +For an example that checks both tenant and record ownership, refer to [Protect data even when generated routes change](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#protect-data-even-when-generated-routes-change). #### Resource isolation diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx index c0c35d08263..3ee6ee43ac1 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx @@ -18,7 +18,7 @@ A [dynamic dispatch Worker](/cloudflare-for-platforms/workers-for-platforms/how- You can also create a dispatch Worker from the Cloudflare dashboard. Go to **Workers for Platforms**, select your namespace, and click **Create** > **Dispatch Worker**. The dashboard provides templates for path-based and subdomain-based routing. ::: -For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/). The guide combines route policies with scoped data access. +For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/). The guide combines route policies with scoped data access. #### Why use a dynamic dispatch Worker? diff --git a/src/content/docs/dynamic-workers/index.mdx b/src/content/docs/dynamic-workers/index.mdx index 66a44df7682..671404b897e 100644 --- a/src/content/docs/dynamic-workers/index.mdx +++ b/src/content/docs/dynamic-workers/index.mdx @@ -40,7 +40,7 @@ Use Dynamic Workers when code needs to run quickly in a secure, isolated environ Because you compose the Worker that runs the code at runtime, you control how that Worker is configured and what it can access. -- **[Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/)**: Enforce route policies and protect data access outside generated code. +- **[Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/)**: Enforce route policies and protect data access outside generated code. - **[Bindings](/dynamic-workers/usage/bindings/)**: Decide which bindings and structured data the dynamic Worker receives. - **[Observability](/dynamic-workers/usage/observability/)**: Attach Tail Workers and capture logs for each run. - **[Network access](/dynamic-workers/usage/egress-control/)**: Intercept or block Internet access for outbound requests. diff --git a/src/content/docs/dynamic-workers/usage/bindings.mdx b/src/content/docs/dynamic-workers/usage/bindings.mdx index 36fbf9d55ad..1da08a8e440 100644 --- a/src/content/docs/dynamic-workers/usage/bindings.mdx +++ b/src/content/docs/dynamic-workers/usage/bindings.mdx @@ -18,7 +18,7 @@ This allows you to: - **Expose custom capabilities**: Define your own methods that Dynamic Workers can call, such as posting to a chat room, sending an email, or querying an internal service. You design the interface, and the Dynamic Worker calls it. - **Restrict and control access**: Inspect, transform, or reject calls before they reach the underlying resource. -For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/best-practices/authenticate-generated-apps/). The guide checks requests before invocation and permissions inside platform-owned operations. +For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/). The guide checks requests before invocation and permissions inside platform-owned operations. ## Custom bindings with Dynamic Workers From 8b6770ca47b2e84d7302927327d92f8ec46ecba3 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 12:57:00 -0700 Subject: [PATCH 09/10] [Workers for Platforms] Frame authentication around customer apps --- public/__redirects | 1 - ...-generated-apps.mdx => authentication.mdx} | 35 +++++++++---------- .../configuration/bindings.mdx | 2 +- .../configuration/dynamic-dispatch.mdx | 2 +- src/content/docs/dynamic-workers/index.mdx | 2 +- .../docs/dynamic-workers/usage/bindings.mdx | 2 +- 6 files changed, 21 insertions(+), 23 deletions(-) rename src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/{authenticate-generated-apps.mdx => authentication.mdx} (88%) diff --git a/public/__redirects b/public/__redirects index 952d647bb12..56f7907c1a9 100644 --- a/public/__redirects +++ b/public/__redirects @@ -1952,7 +1952,6 @@ /cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/email/ /cloudflare-for-platforms/cloudflare-for-saas/security/certificate-management/issue-and-validate/validate-certificates/ 301 # workers for platforms -/workers/best-practices/authenticate-generated-apps/ /cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/ 301 /cloudflare-for-platforms/workers-for-platforms/learning/how-workers-for-platforms-works/ /cloudflare-for-platforms/workers-for-platforms/reference/how-workers-for-platforms-works/ 301 /cloudflare-for-platforms/workers-for-platforms/platform/observability/ /cloudflare-for-platforms/workers-for-platforms/configuration/observability/ 301 /cloudflare-for-platforms/workers-for-platforms/platform/outbound-workers/ /cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/ 301 diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx similarity index 88% rename from src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps.mdx rename to src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx index 15a431d5c5a..4e04d305bad 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx @@ -1,10 +1,9 @@ --- -title: Authenticate and authorize generated apps -description: Enforce route policies before generated code runs, and protect data through scoped capabilities on Workers for Platforms and Dynamic Workers. +title: Authentication and authorization +description: Enforce route policies and data permissions outside customer app code with Workers for Platforms and Dynamic Workers. pcx_content_type: design-guide sidebar: order: 4 - label: Authentication and authorization products: - cloudflare-for-platforms - dynamic-workers @@ -12,11 +11,11 @@ products: import { Tabs, TabItem, TypeScriptExample } from "~/components"; -Your customers can use artificial intelligence (AI) to generate apps. Each app may need both public and private endpoints. +When you build a platform that runs customer apps, you need to control who can use each app and which data they can access. An app may have both public and private endpoints. -Your platform cannot predict every route those apps will introduce. Generated code may also omit authentication or authorization checks. +Customers can add routes and change app code independently of your platform. Keep access rules in platform-owned code, so they do not depend on every app implementing authentication and authorization correctly. -Enforce access in code your platform controls: +Enforce access at two points: - **Before the app runs:** require a list of public and private routes when the app is published. Validate that list and enforce its rules before invoking app code. - **When the app accesses data:** check permissions inside platform-owned operations. Signing in must not let Bob read Alice's private report. @@ -27,7 +26,7 @@ This guide implements these checks with Workers for Platforms and Dynamic Worker flowchart TB Request --> Gate[Platform Worker
Authenticate and check route] Gate -->|Denied| Reject[Return 401, 403, or 404
App does not run] - Gate -->|Allowed| App[Generated app] + Gate -->|Allowed| App[Customer app] App --> Capability[Platform capability
Check access to this record] Capability --> Data[(Private data)] ``` @@ -40,11 +39,11 @@ Choose checks according to what needs protection: | Requirement | Enforcement point | | -------------------------------------------------- | -------------------------------------------- | -| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#authenticate-before-invoking-the-app) | -| Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#publish-code-and-route-policy-together) | -| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#protect-data-even-when-generated-routes-change) | -| Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#adapt-the-pattern) | -| Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#adapt-the-pattern) | +| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#authenticate-before-invoking-the-app) | +| Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#publish-code-and-route-policy-together) | +| Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#protect-data-even-when-routes-change) | +| Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#adapt-the-pattern) | +| Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#adapt-the-pattern) | | App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | Combine route checks with data-operation checks for mixed-access apps. @@ -61,7 +60,7 @@ Your platform does not need to predict every future route. Require each app to d ] ``` -This manifest is a policy format your platform implements. Workers does not infer intended access from generated code. +This manifest is a policy format your platform implements. Workers does not infer intended access from app code. ```mermaid flowchart LR @@ -134,7 +133,7 @@ Implement `authenticate(request)` in `auth.ts` using your existing identity prov - Return a verified identity, or `null` when credentials are absent. - Reject invalid credentials rather than trust caller-supplied identity headers. -Resolve `deployment` from your platform's app registry. Its tenant, routes, code, and script name must not come from the generated app. +Resolve `deployment` from your platform's app registry. Its tenant, routes, code, and script name must not come from app code. Place this gate in the platform request handler: @@ -206,17 +205,17 @@ return app.getEntrypoint(undefined, { props: { identity } }).fetch(appRequest); For a wholly private app, require membership on every request. For mixed-access apps, use the route policy to select public exceptions. -## Protect data even when generated routes change +## Protect data even when routes change A route check can require Bob to sign in, but it cannot prevent app code from requesting Alice's report. Keep permission checks inside the platform operations that read private data, so every new route uses the same checks. -A generated app can omit `checkAuth()` or ignore its result. Give it an operation that performs authorization and data access together. +An app can omit `checkAuth()` or ignore its result. Give it an operation that performs authorization and data access together. ```mermaid sequenceDiagram participant Caller as Bob participant Platform - participant App as Generated app + participant App as Customer app participant Reports as Platform Reports capability Caller->>Platform: Request with Bob's session Platform->>App: Capability scoped to Bob @@ -293,7 +292,7 @@ return app.getEntrypoint(undefined, { props: { reports } }).fetch(appRequest); -The generated app calls the capability through [RPC](/workers/runtime-apis/rpc/): +The customer app calls the capability through [RPC](/workers/runtime-apis/rpc/): ```ts diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx index f4a7c033dc4..3f66b9ed93b 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/bindings.mdx @@ -23,7 +23,7 @@ With bindings, each User Worker can extend functionality to: - **Connect to private networks** with [VPC Services](/workers-vpc/configuration/vpc-services/), [VPC Networks](/workers-vpc/configuration/vpc-networks/), and [Hyperdrive](/hyperdrive/) - **Collect metrics** with [Analytics Engine](/analytics/analytics-engine/) -For an example that checks both tenant and record ownership, refer to [Protect data even when generated routes change](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/#protect-data-even-when-generated-routes-change). +For an example that checks both tenant and record ownership, refer to [Protect data even when routes change](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#protect-data-even-when-routes-change). #### Resource isolation diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx index 3ee6ee43ac1..afef90a1043 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch.mdx @@ -18,7 +18,7 @@ A [dynamic dispatch Worker](/cloudflare-for-platforms/workers-for-platforms/how- You can also create a dispatch Worker from the Cloudflare dashboard. Go to **Workers for Platforms**, select your namespace, and click **Create** > **Dispatch Worker**. The dashboard provides templates for path-based and subdomain-based routing. ::: -For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/). The guide combines route policies with scoped data access. +For apps with public and private routes, refer to [Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/). The guide combines route policies with scoped data access. #### Why use a dynamic dispatch Worker? diff --git a/src/content/docs/dynamic-workers/index.mdx b/src/content/docs/dynamic-workers/index.mdx index 671404b897e..d237e3269ee 100644 --- a/src/content/docs/dynamic-workers/index.mdx +++ b/src/content/docs/dynamic-workers/index.mdx @@ -40,7 +40,7 @@ Use Dynamic Workers when code needs to run quickly in a secure, isolated environ Because you compose the Worker that runs the code at runtime, you control how that Worker is configured and what it can access. -- **[Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/)**: Enforce route policies and protect data access outside generated code. +- **[Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/)**: Enforce route policies and protect data access outside app code. - **[Bindings](/dynamic-workers/usage/bindings/)**: Decide which bindings and structured data the dynamic Worker receives. - **[Observability](/dynamic-workers/usage/observability/)**: Attach Tail Workers and capture logs for each run. - **[Network access](/dynamic-workers/usage/egress-control/)**: Intercept or block Internet access for outbound requests. diff --git a/src/content/docs/dynamic-workers/usage/bindings.mdx b/src/content/docs/dynamic-workers/usage/bindings.mdx index 1da08a8e440..ab30e7e96a5 100644 --- a/src/content/docs/dynamic-workers/usage/bindings.mdx +++ b/src/content/docs/dynamic-workers/usage/bindings.mdx @@ -18,7 +18,7 @@ This allows you to: - **Expose custom capabilities**: Define your own methods that Dynamic Workers can call, such as posting to a chat room, sending an email, or querying an internal service. You design the interface, and the Dynamic Worker calls it. - **Restrict and control access**: Inspect, transform, or reject calls before they reach the underlying resource. -For apps with public and private routes, refer to [Authenticate and authorize generated apps](/cloudflare-for-platforms/workers-for-platforms/configuration/authenticate-generated-apps/). The guide checks requests before invocation and permissions inside platform-owned operations. +For apps with public and private routes, refer to [Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/). The guide checks requests before invocation and permissions inside platform-owned operations. ## Custom bindings with Dynamic Workers From 9d3d9cf88f58262c50aba29bb4f003f891fcded0 Mon Sep 17 00:00:00 2001 From: Brendan Irvine-Broque Date: Thu, 29 Oct 2026 13:04:07 -0700 Subject: [PATCH 10/10] [Workers for Platforms, Dynamic Workers] Separate authentication guides by product --- .../configuration/authentication.mdx | 141 ++---------- src/content/docs/dynamic-workers/index.mdx | 2 +- .../dynamic-workers/usage/authentication.mdx | 210 ++++++++++++++++++ .../docs/dynamic-workers/usage/bindings.mdx | 2 +- .../workers/platform-auth-reports.mdx | 39 ++++ .../workers/platform-auth-route-policy.mdx | 43 ++++ 6 files changed, 311 insertions(+), 126 deletions(-) create mode 100644 src/content/docs/dynamic-workers/usage/authentication.mdx create mode 100644 src/content/partials/workers/platform-auth-reports.mdx create mode 100644 src/content/partials/workers/platform-auth-route-policy.mdx diff --git a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx index 4e04d305bad..f74682aa390 100644 --- a/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx +++ b/src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/authentication.mdx @@ -1,15 +1,14 @@ --- title: Authentication and authorization -description: Enforce route policies and data permissions outside customer app code with Workers for Platforms and Dynamic Workers. +description: Authenticate requests in a dispatch Worker and authorize data access outside customer app code with Workers for Platforms. pcx_content_type: design-guide sidebar: order: 4 products: - cloudflare-for-platforms - - dynamic-workers --- -import { Tabs, TabItem, TypeScriptExample } from "~/components"; +import { Render, TypeScriptExample } from "~/components"; When you build a platform that runs customer apps, you need to control who can use each app and which data they can access. An app may have both public and private endpoints. @@ -20,13 +19,13 @@ Enforce access at two points: - **Before the app runs:** require a list of public and private routes when the app is published. Validate that list and enforce its rules before invoking app code. - **When the app accesses data:** check permissions inside platform-owned operations. Signing in must not let Bob read Alice's private report. -This guide implements these checks with Workers for Platforms and Dynamic Workers. +With Workers for Platforms, check requests in the dispatch Worker and pass scoped data operations to user Workers. ```mermaid flowchart TB - Request --> Gate[Platform Worker
Authenticate and check route] + Request --> Gate[Dispatch Worker
Authenticate and check route] Gate -->|Denied| Reject[Return 401, 403, or 404
App does not run] - Gate -->|Allowed| App[Customer app] + Gate -->|Allowed| App[User Worker] App --> Capability[Platform capability
Check access to this record] Capability --> Data[(Private data)] ``` @@ -39,12 +38,12 @@ Choose checks according to what needs protection: | Requirement | Enforcement point | | -------------------------------------------------- | -------------------------------------------- | -| The whole app requires sign-in | [Dispatch or loader Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#authenticate-before-invoking-the-app) | +| The whole app requires sign-in | [Dispatch Worker, before invocation](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#authenticate-before-invoking-the-app) | | Routes mix public and private access | [Platform-owned method/path policy](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#publish-code-and-route-policy-together) | | Routes are unknown, but data permissions are known | [Platform-owned data operations](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#protect-data-even-when-routes-change) | | Routing and policy must stay together | [Registered handlers with access metadata](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#adapt-the-pattern) | | Public code needs fewer privileges | [Separate Workers with different capabilities](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/#adapt-the-pattern) | -| App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | +| App libraries call HTTP APIs | [Outbound Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) | Combine route checks with data-operation checks for mixed-access apps. @@ -77,49 +76,13 @@ The platform owns these publication rules: - Prevent changes that weaken required permissions. - Keep code and policy versions consistent during rollouts. -For Workers for Platforms, upload a new script name first. Then switch the dispatch Worker to that script and its matching policy. +Upload a new script name first. Then switch the dispatch Worker to that script and its matching policy. ### Deny undeclared routes Match both method and path. A new route stays inaccessible until the platform accepts its policy. - -```ts -export type Identity = { - userId: string; - tenant: string; - permissions: string[]; -}; - -export type Route = { - method: string; - path: string; - access: "public" | "member" | "reports:admin"; -}; - -export function checkRoute( - request: Request, - routes: Route[], - tenant: string, - identity: Identity | null, -): Response | null { - const path = new URL(request.url).pathname; - const route = routes.find( - (route) => route.method === request.method && route.path === path, - ); - if (!route) return new Response("Route not published", { status: 404 }); - if (route.access === "public") return null; - if (!identity) return new Response("Sign in", { status: 401 }); - if ( - identity.tenant !== tenant || - (route.access !== "member" && !identity.permissions.includes(route.access)) - ) { - return new Response("Forbidden", { status: 403 }); - } - return null; -} -``` - + Here, `member` means membership in the app tenant. Add an app-membership check if only some tenant members may enter. @@ -133,9 +96,9 @@ Implement `authenticate(request)` in `auth.ts` using your existing identity prov - Return a verified identity, or `null` when credentials are absent. - Reject invalid credentials rather than trust caller-supplied identity headers. -Resolve `deployment` from your platform's app registry. Its tenant, routes, code, and script name must not come from app code. +Resolve `deployment` from your platform's app registry. Its tenant, routes, and script name must not come from app code. -Place this gate in the platform request handler: +Place this gate in the dispatch Worker request handler: ```ts @@ -163,12 +126,7 @@ const appRequest = new Request(request.url, { method: "GET" }); -Invoke the app only after the gate succeeds: - - - - -Use the [dispatch namespace binding](/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/): +After the gate succeeds, invoke the app through the [dispatch namespace binding](/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/): ```ts @@ -181,28 +139,6 @@ return app.fetch(appRequest); All app ingress must pass through the dispatch Worker. - - - -Authenticate in the parent Worker before calling the [Worker Loader](/dynamic-workers/api-reference/): - - -```ts -const app = env.LOADER.load({ - compatibilityDate: "$today", - mainModule: "app.js", - modules: { "app.js": deployment.code }, - globalOutbound: null, -}); -return app.getEntrypoint(undefined, { props: { identity } }).fetch(appRequest); -``` - - -`load()` creates a fresh Worker. With `get()`, keep code and configuration identical for each cache ID; do not cache one visitor's credentials for other visitors. - - - - For a wholly private app, require membership on every request. For mixed-access apps, use the route policy to select public exceptions. ## Protect data even when routes change @@ -214,8 +150,8 @@ An app can omit `checkAuth()` or ignore its result. Give it an operation that pe ```mermaid sequenceDiagram participant Caller as Bob - participant Platform - participant App as Customer app + participant Platform as Dispatch Worker + participant App as User Worker participant Reports as Platform Reports capability Caller->>Platform: Request with Bob's session Platform->>App: Capability scoped to Bob @@ -224,41 +160,9 @@ sequenceDiagram App-->>Caller: 404 ``` -Keep the data and implementation in the platform Worker. This example uses synthetic records: +Keep the data and implementation in the dispatch Worker. This example uses synthetic records: - -```ts -import { WorkerEntrypoint } from "cloudflare:workers"; - -const records = [ - { id: "alice", tenant: "acme", owner: "alice", text: "Alice's report" }, - { id: "bob", tenant: "acme", owner: "bob", text: "Bob's report" }, -]; - -type Scope = { identity: Identity | null; tenant: string }; - -export class Reports extends WorkerEntrypoint { - read(id: string) { - const { identity, tenant } = this.ctx.props; - if (!identity) return { status: 401, error: "Sign in" }; - if ( - identity.tenant !== tenant || - !identity.permissions.includes("reports:read") - ) { - return { status: 403, error: "Forbidden" }; - } - const report = records.find( - (record) => - record.id === id && - record.tenant === tenant && - record.owner === identity.userId, - ); - if (!report) return { status: 404, error: "Report not found" }; - return { status: 200, report: { text: report.text } }; - } -} -``` - + Create the capability using verified platform context. The app can choose a record ID, but cannot choose the capability's identity: @@ -272,8 +176,6 @@ const reports = ctx.exports.Reports({ Pass the capability with the invocation: - - ```ts const app = env.DISPATCHER.get(deployment.scriptName, { @@ -282,15 +184,6 @@ const app = env.DISPATCHER.get(deployment.scriptName, { return app.fetch(appRequest); ``` - - - -```ts -return app.getEntrypoint(undefined, { props: { reports } }).fetch(appRequest); -``` - - - The customer app calls the capability through [RPC](/workers/runtime-apis/rpc/): @@ -301,7 +194,7 @@ return Response.json(result, { status: result.status }); ```
-Block unrestricted network access, or mediate it through [Outbound Workers](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) or [Dynamic Workers egress controls](/dynamic-workers/usage/egress-control/). Otherwise, another data-access path could bypass the capability. +Use [Outbound Workers](/cloudflare-for-platforms/workers-for-platforms/configuration/outbound-workers/) to restrict network access to authorized destinations and operations. Otherwise, another data-access path could bypass the capability. ## Adapt the pattern diff --git a/src/content/docs/dynamic-workers/index.mdx b/src/content/docs/dynamic-workers/index.mdx index d237e3269ee..ea3af625ad1 100644 --- a/src/content/docs/dynamic-workers/index.mdx +++ b/src/content/docs/dynamic-workers/index.mdx @@ -40,7 +40,7 @@ Use Dynamic Workers when code needs to run quickly in a secure, isolated environ Because you compose the Worker that runs the code at runtime, you control how that Worker is configured and what it can access. -- **[Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/)**: Enforce route policies and protect data access outside app code. +- **[Authentication and authorization](/dynamic-workers/usage/authentication/)**: Enforce route policies and protect data access outside app code. - **[Bindings](/dynamic-workers/usage/bindings/)**: Decide which bindings and structured data the dynamic Worker receives. - **[Observability](/dynamic-workers/usage/observability/)**: Attach Tail Workers and capture logs for each run. - **[Network access](/dynamic-workers/usage/egress-control/)**: Intercept or block Internet access for outbound requests. diff --git a/src/content/docs/dynamic-workers/usage/authentication.mdx b/src/content/docs/dynamic-workers/usage/authentication.mdx new file mode 100644 index 00000000000..555823887d0 --- /dev/null +++ b/src/content/docs/dynamic-workers/usage/authentication.mdx @@ -0,0 +1,210 @@ +--- +title: Authentication and authorization +description: Authenticate requests in a loader Worker and authorize data access outside customer app code with Dynamic Workers. +pcx_content_type: design-guide +sidebar: + order: 2 +products: + - dynamic-workers +--- + +import { Render, TypeScriptExample } from "~/components"; + +When you build a platform that runs customer apps, you need to control who can use each app and which data they can access. An app may have both public and private endpoints. + +Customers can add routes and change app code independently of your platform. Keep access rules in platform-owned code, so they do not depend on every app implementing authentication and authorization correctly. + +Enforce access at two points: + +- **Before the app runs:** require a list of public and private routes when the app is published. Validate that list and enforce its rules before invoking app code. +- **When the app accesses data:** check permissions inside platform-owned operations. Signing in must not let Bob read Alice's private report. + +With Dynamic Workers, check requests in the loader Worker and pass scoped data operations to the Worker it loads. + +```mermaid +flowchart TB + Request --> Gate[Loader Worker
Authenticate and check route] + Gate -->|Denied| Reject[Return 401, 403, or 404
App does not run] + Gate -->|Allowed| App[Dynamic Worker] + App --> Capability[Platform capability
Check access to this record] + Capability --> Data[(Private data)] +``` + +Authentication establishes identity. Authorization determines which routes, actions, and records that identity can access. + +## Choose where to enforce access + +Choose checks according to what needs protection: + +| Requirement | Enforcement point | +| -------------------------------------------------- | -------------------------------------------- | +| The whole app requires sign-in | [Loader Worker, before invocation](/dynamic-workers/usage/authentication/#authenticate-before-invoking-the-app) | +| Routes mix public and private access | [Platform-owned method/path policy](/dynamic-workers/usage/authentication/#publish-code-and-route-policy-together) | +| Routes are unknown, but data permissions are known | [Platform-owned data operations](/dynamic-workers/usage/authentication/#protect-data-even-when-routes-change) | +| Routing and policy must stay together | [Registered handlers with access metadata](/dynamic-workers/usage/authentication/#adapt-the-pattern) | +| Public code needs fewer privileges | [Separate Workers with different capabilities](/dynamic-workers/usage/authentication/#adapt-the-pattern) | +| App libraries call HTTP APIs | [`globalOutbound` gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests) | + +Combine route checks with data-operation checks for mixed-access apps. + +## Publish code and route policy together + +Your platform does not need to predict every future route. Require each app to declare its routes when it publishes: + +```json title="routes.json" +[ + { "method": "GET", "path": "/", "access": "public" }, + { "method": "GET", "path": "/profile", "access": "member" }, + { "method": "GET", "path": "/admin", "access": "reports:admin" } +] +``` + +This manifest is a policy format your platform implements. Workers does not infer intended access from app code. + +```mermaid +flowchart LR + Upload[Customer submits code + policy] --> Validate[Platform validates policy] + Validate --> Release[Version code + policy together] + Release --> Publish[Publish the matching release] +``` + +The platform owns these publication rules: + +- Match the tenant to the authenticated app owner. +- Reject duplicate or ambiguous routes. +- Restrict which routes can become public. +- Prevent changes that weaken required permissions. +- Keep code and policy versions consistent during rollouts. + +Store each app's code and route policy as one versioned deployment. Resolve both from that deployment before calling the loader. + +### Deny undeclared routes + +Match both method and path. A new route stays inaccessible until the platform accepts its policy. + + + +Here, `member` means membership in the app tenant. Add an app-membership check if only some tenant members may enter. + +Use one path interpretation across the gate and app. This example matches exact URL paths; it grants no wildcard access. + +## Authenticate before invoking the app + +Implement `authenticate(request)` in `auth.ts` using your existing identity provider or session service. This integration must: + +- Verify the session or token, including expiry and intended audience. +- Return a verified identity, or `null` when credentials are absent. +- Reject invalid credentials rather than trust caller-supplied identity headers. + +Resolve `deployment` from your platform's app registry. Its tenant, routes, and code must not come from app code. + +Place this gate in the loader Worker request handler: + + +```ts +import { checkRoute, type Identity } from "./policy"; +import { authenticate } from "./auth"; + +let identity: Identity | null; +try { + identity = await authenticate(request); +} catch { + return new Response("Invalid session", { status: 401 }); +} + +const denied = checkRoute( + request, + deployment.routes, + deployment.tenant, + identity, +); +if (denied) return denied; + +// Forward only what this GET-only example needs. Keep session credentials out. +const appRequest = new Request(request.url, { method: "GET" }); +``` + + + +After the gate succeeds, load and invoke the app with the [Worker Loader](/dynamic-workers/api-reference/): + + +```ts +const app = env.LOADER.load({ + compatibilityDate: "$today", + mainModule: "app.js", + modules: { "app.js": deployment.code }, + globalOutbound: null, +}); +return app.getEntrypoint(undefined, { props: { identity } }).fetch(appRequest); +``` + + +`load()` creates a fresh Worker. With `get()`, keep code and configuration identical for each cache ID; do not cache one visitor's credentials for other visitors. + +For a wholly private app, require membership on every request. For mixed-access apps, use the route policy to select public exceptions. + +## Protect data even when routes change + +A route check can require Bob to sign in, but it cannot prevent app code from requesting Alice's report. Keep permission checks inside the platform operations that read private data, so every new route uses the same checks. + +An app can omit `checkAuth()` or ignore its result. Give it an operation that performs authorization and data access together. + +```mermaid +sequenceDiagram + participant Caller as Bob + participant Platform as Loader Worker + participant App as Dynamic Worker + participant Reports as Platform Reports capability + Caller->>Platform: Request with Bob's session + Platform->>App: Capability scoped to Bob + App->>Reports: read("alice") + Reports-->>App: 404 — record does not belong to Bob + App-->>Caller: 404 +``` + +Keep the data and implementation in the loader Worker. This example uses synthetic records: + + + +Create the capability using verified platform context. The app can choose a record ID, but cannot choose the capability's identity: + + +```ts +const reports = ctx.exports.Reports({ + props: { identity, tenant: deployment.tenant }, +}); +``` + + +Pass the capability with the invocation: + + +```ts +return app.getEntrypoint(undefined, { props: { reports } }).fetch(appRequest); +``` + + +The customer app calls the capability through [RPC](/workers/runtime-apis/rpc/): + + +```ts +const result = await this.ctx.props.reports.read(reportId); +return Response.json(result, { status: result.status }); +``` + + +Keep `globalOutbound: null` to block network access, or mediate requests through a [gateway](/dynamic-workers/usage/egress-control/#intercept-outbound-requests). Otherwise, another data-access path could bypass the capability. + +## Adapt the pattern + +Use these variations when the app interface requires them: + +| Pattern | Example | Why | +| --------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| Registered handlers | `{ method: "GET", path: "/profile", access: "member", handler: "profile" }` | Select policy and handler from the same platform-owned entry. A handler name does not grant data access. | +| Public app shell | Public HTML; protected report reads through `reports.read(id)` | The platform can protect data without enumerating every frontend route. | +| Separate components | Public Dynamic Worker receives `{}`; private Dynamic Worker receives `{ reports }` | Public code has no private capability. Keep their storage separate too. | +| Outbound HTTP gateway | Allow `GET https://reports.internal/reports/:id`, then check record access | Existing HTTP clients can use the same authorization model. Ignore identity headers supplied by app code. | + +If the outbound gateway injects credentials, authorize the destination and operation first. Hiding a token does not limit what that token can do. diff --git a/src/content/docs/dynamic-workers/usage/bindings.mdx b/src/content/docs/dynamic-workers/usage/bindings.mdx index ab30e7e96a5..c4dbe9ed4b2 100644 --- a/src/content/docs/dynamic-workers/usage/bindings.mdx +++ b/src/content/docs/dynamic-workers/usage/bindings.mdx @@ -18,7 +18,7 @@ This allows you to: - **Expose custom capabilities**: Define your own methods that Dynamic Workers can call, such as posting to a chat room, sending an email, or querying an internal service. You design the interface, and the Dynamic Worker calls it. - **Restrict and control access**: Inspect, transform, or reject calls before they reach the underlying resource. -For apps with public and private routes, refer to [Authentication and authorization](/cloudflare-for-platforms/workers-for-platforms/configuration/authentication/). The guide checks requests before invocation and permissions inside platform-owned operations. +For apps with public and private routes, refer to [Authentication and authorization](/dynamic-workers/usage/authentication/). The guide checks requests before invocation and permissions inside platform-owned operations. ## Custom bindings with Dynamic Workers diff --git a/src/content/partials/workers/platform-auth-reports.mdx b/src/content/partials/workers/platform-auth-reports.mdx new file mode 100644 index 00000000000..68b017d05b2 --- /dev/null +++ b/src/content/partials/workers/platform-auth-reports.mdx @@ -0,0 +1,39 @@ +--- +{} +--- + +import { TypeScriptExample } from "~/components"; + + +```ts +import { WorkerEntrypoint } from "cloudflare:workers"; + +const records = [ + { id: "alice", tenant: "acme", owner: "alice", text: "Alice's report" }, + { id: "bob", tenant: "acme", owner: "bob", text: "Bob's report" }, +]; + +type Scope = { identity: Identity | null; tenant: string }; + +export class Reports extends WorkerEntrypoint { + read(id: string) { + const { identity, tenant } = this.ctx.props; + if (!identity) return { status: 401, error: "Sign in" }; + if ( + identity.tenant !== tenant || + !identity.permissions.includes("reports:read") + ) { + return { status: 403, error: "Forbidden" }; + } + const report = records.find( + (record) => + record.id === id && + record.tenant === tenant && + record.owner === identity.userId, + ); + if (!report) return { status: 404, error: "Report not found" }; + return { status: 200, report: { text: report.text } }; + } +} +``` + diff --git a/src/content/partials/workers/platform-auth-route-policy.mdx b/src/content/partials/workers/platform-auth-route-policy.mdx new file mode 100644 index 00000000000..d95e8177b8a --- /dev/null +++ b/src/content/partials/workers/platform-auth-route-policy.mdx @@ -0,0 +1,43 @@ +--- +{} +--- + +import { TypeScriptExample } from "~/components"; + + +```ts +export type Identity = { + userId: string; + tenant: string; + permissions: string[]; +}; + +export type Route = { + method: string; + path: string; + access: "public" | "member" | "reports:admin"; +}; + +export function checkRoute( + request: Request, + routes: Route[], + tenant: string, + identity: Identity | null, +): Response | null { + const path = new URL(request.url).pathname; + const route = routes.find( + (route) => route.method === request.method && route.path === path, + ); + if (!route) return new Response("Route not published", { status: 404 }); + if (route.access === "public") return null; + if (!identity) return new Response("Sign in", { status: 401 }); + if ( + identity.tenant !== tenant || + (route.access !== "member" && !identity.permissions.includes(route.access)) + ) { + return new Response("Forbidden", { status: 403 }); + } + return null; +} +``` +