Skip to content

Commit b8fd112

Browse files
authored
feat: implement local dev for ctx.access (#15113)
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
1 parent f0f2054 commit b8fd112

19 files changed

Lines changed: 836 additions & 11 deletions

File tree

.changeset/access-local-dev.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"miniflare": minor
3+
"wrangler": minor
4+
---
5+
6+
Add local dev simulation for Cloudflare Access `ctx.access.getIdentity()`
7+
8+
You can now configure a mock Cloudflare Access identity in `wrangler.json` so that `ctx.access.getIdentity()` returns it during local development.
9+
10+
```jsonc
11+
// wrangler.json
12+
{
13+
"access": {
14+
"dev": {
15+
"aud": "my-app-aud-tag",
16+
"identity": {
17+
"email": "user@example.com",
18+
"name": "Test User",
19+
},
20+
},
21+
},
22+
}
23+
```

packages/miniflare/src/config/schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,13 @@ export const DevConfigSchema = z.strictObject({
620620
// Zone to use for the CF-Worker header in outbound fetches. If not
621621
// specified, defaults to `${worker-name}.example.com`
622622
zone: z.string().optional(),
623+
/** Cloudflare Access authentication metadata exposed as `ctx.access` */
624+
access: z
625+
.strictObject({
626+
aud: z.string(),
627+
identity: z.record(z.string(), z.unknown()).optional(),
628+
})
629+
.optional(),
623630
});
624631

625632
export type DevConfig = z.input<typeof DevConfigSchema>;

packages/miniflare/src/config/v4-convert.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ function convertWorkerOptions(
184184
dev.unsafeEphemeralDurableObjects = worker.unsafeEphemeralDurableObjects;
185185
dev.stripCfConnectingIp = worker.stripCfConnectingIp;
186186
dev.zone = worker.zone;
187+
dev.access = worker.access;
187188

188189
const options: WorkerOptions = { config };
189190
if (Object.values(legacy).some((value) => value !== undefined)) {

packages/miniflare/src/config/v4-schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,13 @@ const V4WorkerOptionsShapeSchema = z.object({
373373
streamingTails: z.array(V4ServiceDesignatorSchema).optional(),
374374
stripCfConnectingIp: z.boolean().default(true),
375375
zone: z.string().optional(),
376+
/** Cloudflare Access authentication metadata exposed as `ctx.access` */
377+
access: z
378+
.object({
379+
aud: z.string(),
380+
identity: z.record(z.string(), z.unknown()).optional(),
381+
})
382+
.optional(),
376383
unsafeBindings: z
377384
.array(
378385
z.object({
@@ -780,6 +787,7 @@ export type V4WorkerOptionsShape = {
780787
streamingTails?: V4ServiceDesignator[];
781788
stripCfConnectingIp?: boolean;
782789
zone?: string;
790+
access?: { aud: string; identity?: Record<string, unknown> };
783791
unsafeBindings?: Array<{
784792
name: string;
785793
type: string;

packages/miniflare/src/plugins/core/index.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
removeDirSync,
1919
stripRedundantNodejsCompatFlags,
2020
} from "@cloudflare/workers-utils";
21+
import SCRIPT_ACCESS_IDENTITY from "worker:access/access-identity";
2122
import SCRIPT_DEV_CONTROL from "worker:core/dev-control";
2223
import SCRIPT_ENTRY from "worker:core/entry";
2324
import OUTBOUND_WORKER from "worker:core/outbound";
@@ -308,6 +309,10 @@ function getDevControlBindings(
308309
return Array.from(bindings.values());
309310
}
310311

312+
function getAccessIdentityServiceName(workerIndex: number) {
313+
return `access-identity:${workerIndex}`;
314+
}
315+
311316
function getOutboundInterceptorName(workerIndex: number) {
312317
return `outbound:${workerIndex}`;
313318
}
@@ -653,6 +658,14 @@ export const CORE_PLUGIN: Plugin = {
653658
.filter((consumer) => consumer.streaming)
654659
.map<ServiceDesignator>(getTailServiceDesignator),
655660
containerEngine: getContainerEngine(sharedOptions.containerEngine),
661+
...(dev?.access
662+
? {
663+
accessBlobHeader: CoreHeaders.ACCESS_BLOB,
664+
accessBindingService: {
665+
name: getAccessIdentityServiceName(workerIndex),
666+
},
667+
}
668+
: {}),
656669
},
657670
});
658671

@@ -710,6 +723,26 @@ export const CORE_PLUGIN: Plugin = {
710723
});
711724
}
712725

726+
// Access identity binding worker for ctx.access.getIdentity()
727+
if (dev?.access) {
728+
services.push({
729+
name: getAccessIdentityServiceName(workerIndex),
730+
worker: {
731+
modules: [
732+
{
733+
name: "index.js",
734+
esModule: SCRIPT_ACCESS_IDENTITY(),
735+
},
736+
],
737+
compatibilityDate: "2025-01-01",
738+
compatibilityFlags: [
739+
"experimental",
740+
"service_binding_extra_handlers",
741+
],
742+
},
743+
});
744+
}
745+
713746
return { services, extensions };
714747
},
715748
};
@@ -857,6 +890,31 @@ export function getGlobalServices({
857890
data: encoder.encode(sharedOptions.unsafeProxySharedSecret),
858891
});
859892
}
893+
// Inject per-worker Cloudflare Access blob bindings into the entry worker.
894+
// Each worker with dev.access gets its own blob keyed by worker name so the
895+
// entry worker can pick the correct one after routing.
896+
for (const workerOpt of allWorkerOpts ?? []) {
897+
const accessOpts = workerOpt.dev?.access;
898+
if (accessOpts) {
899+
const accessBlob: {
900+
app_aud: string;
901+
jwt_claims?: Record<string, unknown>;
902+
} = { app_aud: accessOpts.aud };
903+
if (accessOpts.identity) {
904+
accessBlob.jwt_claims = accessOpts.identity;
905+
}
906+
serviceEntryBindings.push({
907+
name: CoreBindings.JSON_ACCESS_BLOB_PREFIX + workerOpt.config.name,
908+
json: JSON.stringify(accessBlob),
909+
});
910+
}
911+
}
912+
// Pass the first worker's raw name so the entry worker can look up its
913+
// access blob when no route matches (the fallback is always the first worker).
914+
serviceEntryBindings.push({
915+
name: CoreBindings.TEXT_FALLBACK_WORKER_NAME,
916+
text: workerNames[0] ?? "",
917+
});
860918
const services: Service[] = [
861919
{
862920
name: SERVICE_LOOPBACK,

0 commit comments

Comments
 (0)