Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/development-oidc-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"miniflare": minor
"wrangler": minor
"@cloudflare/vite-plugin": minor
"@cloudflare/vitest-plugin": minor
---

Connect authenticated local Workers OIDC to the native token getter

Share lazy per-Worker brokers, authenticated development sessions, bounded API responses, token profile validation, current authorization and on-demand renewal across Wrangler, `cf dev`, and the Cloudflare Vite plugin. Keep OAuth credentials in the host process and display the server-authorized development subject. Vitest live issuance requires `developmentOidc: true`; offline tests can supply a host broker mock.

Retain authorized development sessions after temporary network, signing-service, rate-limit, and timeout failures. Honor `Retry-After` for issuance requests, and clear sessions and pending requests after login, permission, renewal, or invalid-response failures.

This experimental integration requires a workerd release containing `Worker.oidcTokenService` and Cloudflare's development issuance service, including its human OAuth verifier and API gateway routes. The current runtime pin predates the callback; a matching local workerd build can be exercised using `MINIFLARE_WORKERD_PATH` until that release is available.
10 changes: 10 additions & 0 deletions .changeset/development-oidc-primitives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"miniflare": minor
"@cloudflare/workers-auth": minor
"wrangler": patch
"@cloudflare/vite-plugin": patch
---

Add shared host-side development OIDC session and bounded issuance primitives, and honor the authentication profile forwarded by `cf dev` in shared development authentication. Native local OIDC issuance still requires the matching workerd callback and Cloudflare development issuance API integrations.

Bound host request lifetimes, pending callers, and UTF-8 response data. Invalidation rejects callers promptly, including when a transport ignores abort, while retaining concurrency accounting for unfinished operations. Snapshot credentials and session metadata, and use the selected OAuth credential for account discovery.
9 changes: 9 additions & 0 deletions .changeset/oidc-validation-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/workers-utils": minor
"@cloudflare/remote-bindings": minor
"miniflare": minor
---

Reject malformed audiences consistently in experimental Workers OIDC

Use the signer's exact URI grammar in configuration and local issuance. Reject hidden binding fields, malformed identifiers, and whitespace-only audiences before requesting a session or token.
17 changes: 17 additions & 0 deletions .changeset/workload-identity-bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@cloudflare/config": minor
"@cloudflare/workers-utils": minor
"@cloudflare/deploy-helpers": minor
"miniflare": minor
"wrangler": minor
"@cloudflare/vite-plugin": minor
"@cloudflare/vitest-plugin": minor
---

Replace implicit context OIDC access with explicit identity bindings.

Configure an existing identity with its approved audience policy, then call `env.IDENTITY.getToken({ audience, fresh })`. Preserve bindings through cf configuration, uploads, Previews, local runtime configuration, and generated types. Configuration requests an approved assignment; it cannot create one.

Recheck OAuth, Worker access, and the current local assignment on every getter. Keep session renewal lazy, forward every fresh request separately, and reject sessions that do not match the configured identity and audience policy. Local Durable Objects use the local identity profile. Vitest still requires an explicit live-issuance opt-in.

Native execution requires the matching workerd identity-binding implementation and Cloudflare authorization services. Until a runtime release includes those changes, use `MINIFLARE_WORKERD_PATH` with that build.
1 change: 1 addition & 0 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Prerelease

on:
workflow_dispatch:
pull_request:
push:
branches:
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
"test:watch": "vitest"
},
"dependencies": {
"@cloudflare/workers-utils": "workspace:*",
"zod": "catalog:default"
},
"devDependencies": {
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "catalog:default",
"@cloudflare/workers-utils": "workspace:*",
"ts-dedent": "^2.2.0",
"tsdown": "0.16.3",
"typescript": "catalog:default",
Expand Down
33 changes: 33 additions & 0 deletions packages/config/src/__tests__/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1744,3 +1744,36 @@ describe("convertToWranglerConfig", () => {
});
});
});

it("preserves explicit OIDC identities when converting cf configuration", ({
expect,
}) => {
const result = convertToWranglerConfig({
worker: {
...baseWorker,
env: {
ORDERS: {
type: "oidc",
identity_id: "orders",
audience: "urn:orders",
allowed_audiences: ["urn:orders", "urn:reports"],
},
REPORTS: {
type: "oidc",
identity_id: "reports",
audience: "recipient",
},
},
},
containers: [],
});
expect(result.oidc).toEqual([
{
binding: "ORDERS",
identity_id: "orders",
audience: "urn:orders",
allowed_audiences: ["urn:orders", "urn:reports"],
},
{ binding: "REPORTS", identity_id: "reports", audience: "recipient" },
]);
});
57 changes: 57 additions & 0 deletions packages/config/src/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1753,3 +1753,60 @@ describe("ExportSchema", () => {
expect(typeAssertions).toBeTypeOf("function");
});
});

describe("OIDC identity bindings", () => {
it("accepts several explicit identities and exact audience policies", ({
expect,
}) => {
const result = InputWorkerSchema.parse({
...baseConfig,
env: {
ORDERS: {
type: "oidc",
identity_id: "orders",
audience: "urn:orders",
allowed_audiences: ["urn:orders", "urn:reports"],
},
REPORTS: {
type: "oidc",
identity_id: "reports",
audience: "recipient",
},
},
});
expect(result.env?.ORDERS).toMatchObject({
identity_id: "orders",
audience: "urn:orders",
});
});
it.for([
{ identity_id: "orders", audience: " " },
{ identity_id: "orders", audience: "recipient", allowed_audiences: [] },
{
identity_id: "orders",
audience: "recipient",
allowed_audiences: ["other"],
},
{
identity_id: "orders",
audience: "recipient",
allowed_audiences: ["recipient", "recipient"],
},
{
identity_id: "orders",
audience: "recipient",
assignment_id: "copied-grant",
},
{ identity_id: "orders", audience: "recipient", subject: "identity:admin" },
])(
"rejects unapproved configuration fields or invalid audiences: %j",
(binding, { expect }) => {
expect(
InputWorkerSchema.safeParse({
...baseConfig,
env: { IDENTITY: { type: "oidc", ...binding } },
}).success
).toBe(false);
}
);
});
12 changes: 12 additions & 0 deletions packages/config/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,16 @@ export interface R2Binding extends R2BindingOptions {
type: "r2";
}

/** Requests an existing approved identity assignment. This configuration never grants access. */
export interface OidcBindingOptions {
identity_id: string;
audience: string;
allowed_audiences?: string[];
}
export interface OidcBinding extends OidcBindingOptions {
type: "oidc";
}

export interface RateLimitBindingOptions {
/** The namespace ID for this rate limiter. */
namespace: string;
Expand Down Expand Up @@ -847,6 +857,7 @@ export interface Bindings {
*/
r2(options?: R2BindingOptions): R2Binding;
/** Binding to a rate limiter. */
oidc(options: OidcBindingOptions): OidcBinding;
rateLimit(options: RateLimitBindingOptions): RateLimitBinding;
/**
* Declares a secret that is required by your Worker, exposed on `env` under
Expand Down Expand Up @@ -952,6 +963,7 @@ export const bindings = {
pipeline: (options) => ({ type: "pipeline", ...options }),
k2: (options) => ({ type: "k2", ...options }),
queue: (options) => ({ type: "queue", ...options }),
oidc: (options) => ({ type: "oidc", ...options }),
rateLimit: (options) => ({ type: "rate-limit", ...options }),
r2: (options) => ({ type: "r2", ...options }),
secret: () => ({ type: "secret" }),
Expand Down
11 changes: 11 additions & 0 deletions packages/config/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,17 @@ function convertBindingsAndAssets(
}

switch (binding.type) {
case "oidc": {
(result.oidc ??= []).push({
binding: name,
identity_id: binding.identity_id,
audience: binding.audience,
...(binding.allowed_audiences === undefined
? {}
: { allowed_audiences: binding.allowed_audiences }),
});
break;
}
case "agent-memory": {
agentMemory.push(
omitUndefined({
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ interface BindingTypeMap<TBinding> {
logfwdr: any;
media: MediaBinding;
"mtls-certificate": Fetcher;
oidc: {
getToken(options?: { audience?: string; fresh?: boolean }): Promise<string>;
};
"rate-limit": RateLimit;
r2: R2Bucket;
secret: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type {
PipelineBindingOptions,
QueueBinding,
QueueBindingOptions,
OidcBinding,
OidcBindingOptions,
RateLimitBinding,
RateLimitBindingOptions,
R2Binding,
Expand Down
20 changes: 20 additions & 0 deletions packages/config/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isOidcBinding } from "@cloudflare/workers-utils/oidc";
import * as z from "zod";
import type { SendEmailBinding, VpcNetworkBinding } from "./bindings";
import type { Settings } from "./types";
Expand Down Expand Up @@ -103,6 +104,25 @@ export const HyperdriveBindingSchema = z.strictObject({
});

export const KnownBindingSchema = z.discriminatedUnion("type", [
z
.strictObject({
type: z.literal("oidc"),
identity_id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/),
audience: z.string().min(1).max(2048),
allowed_audiences: z
.array(z.string().min(1).max(2048))
.min(1)
.max(64)
.optional(),
})
.refine(
({ type: _type, ...binding }) =>
isOidcBinding({ binding: "IDENTITY", ...binding }),
{
message:
"OIDC requires valid exact audiences and an allowed list containing its default.",
}
),
z.strictObject({
type: z.literal("agent-memory"),
namespace: z.string(),
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
PipelineBinding,
QueueBinding,
R2Binding,
OidcBinding,
RateLimitBinding,
SecretBinding,
SecretsStoreSecretBinding,
Expand Down Expand Up @@ -115,6 +116,7 @@ type Binding =
| PipelineBinding
| QueueBinding
| R2Binding
| OidcBinding
| RateLimitBinding
| SecretBinding
| SecretsStoreSecretBinding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const reorderableBindings = {
k2: true,
secrets_store_secrets: true,
artifacts: true,
oidc: true,
ratelimits: true,
analytics_engine_datasets: true,
unsafe_hello_world: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,20 @@ export function createWorkerUploadForm(
);
});

for (const {
binding,
identity_id,
audience,
allowed_audiences,
} of extractBindingsOfType("oidc", bindings)) {
metadataBindings.push({
type: "oidc",
name: binding,
identity_id,
audience,
...(allowed_audiences === undefined ? {} : { allowed_audiences }),
});
}
ratelimits.forEach(({ name, namespace_id, simple }) => {
metadataBindings.push({
name,
Expand Down
3 changes: 3 additions & 0 deletions packages/deploy-helpers/src/preview/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import type {
} from "@cloudflare/workers-utils";

export interface Binding {
identity_id?: string;
audience?: string;
allowed_audiences?: string[];
type: string;
text?: string;
json?: Json;
Expand Down
3 changes: 3 additions & 0 deletions packages/deploy-helpers/src/preview/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,9 @@ function extractBindings(
};
}

for (const { binding, ...value } of previews?.oidc ?? []) {
env[binding] = { type: "oidc", ...value };
}
for (const ratelimit of previews?.ratelimits ?? []) {
env[ratelimit.name] = {
type: "ratelimit",
Expand Down
8 changes: 8 additions & 0 deletions packages/deploy-helpers/src/startup-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ type MiniflareBindingOptions = Pick<
| "k2"
| "queueProducers"
| "r2Buckets"
| "oidc"
| "ratelimits"
| "secretsStoreSecrets"
| "serviceBindings"
Expand Down Expand Up @@ -368,6 +369,7 @@ async function convertWorkerBundleBindings(
{ queueName: string; deliveryDelay?: number }
> = {};
const r2Buckets: Record<string, { id: string }> = {};
const oidc: MiniflareBindingOption<"oidc"> = {};
const ratelimits: MiniflareBindingOption<"ratelimits"> = {};
const secretsStoreSecrets: MiniflareBindingOption<"secretsStoreSecrets"> = {};
const serviceBindings: NonNullable<V4WorkerOptionsShape["serviceBindings"]> =
Expand Down Expand Up @@ -538,6 +540,11 @@ async function convertWorkerBundleBindings(
case "flagship":
flagship[binding.name] = { app_id: binding.app_id };
break;
case "oidc": {
const { name, type: _type, ...request } = binding;
oidc[name] = request;
break;
}
case "ratelimit":
ratelimits[binding.name] = {
namespace_id: binding.namespace_id,
Expand Down Expand Up @@ -611,6 +618,7 @@ async function convertWorkerBundleBindings(
k2,
queueProducers,
r2Buckets,
oidc,
ratelimits,
secretsStoreSecrets,
serviceBindings,
Expand Down
2 changes: 1 addition & 1 deletion packages/miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"scripts": {
"build": "node scripts/build.mjs && pnpm run types:build",
"capnp:workerd": "capnp-es node_modules/workerd/workerd.capnp -ots:src/runtime/config/generated --src-prefix=node_modules/workerd",
"capnp:workerd": "node scripts/generate-workerd-config.mjs",
"check:generate-api": "node -r esbuild-register scripts/check-generate-api.ts",
"check:type": "tsc",
"clean": "node -r esbuild-register ../../tools/clean/clean.ts ./dist ./dist-types",
Expand Down
Loading
Loading