Skip to content

Commit 057152e

Browse files
committed
chore: switch to access info
this should be more in line with workerd architecture. we separate the polymorphism to something that AccessContext wraps around rather than having AccessContext itself be polymorphic. Signed-off-by: Matt Provost <mprovost@cloudflare.com>
1 parent 3eddbfa commit 057152e

13 files changed

Lines changed: 118 additions & 31 deletions

src/workerd/api/global-scope.c++

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <workerd/api/tracing.h>
2222
#include <workerd/api/util.h>
2323
#include <workerd/api/worker-rpc.h>
24+
#include <workerd/io/access-info.h>
2425
#include <workerd/io/compatibility-date.h>
2526
#include <workerd/io/features.h>
2627
#include <workerd/io/io-context.h>
@@ -97,18 +98,27 @@ jsg::Optional<jsg::Ref<Tracing>> ExecutionContext::getTracing(jsg::Lock& js) {
9798
}
9899

99100
kj::StringPtr AccessContext::getAud() {
100-
JSG_FAIL_REQUIRE(Error, "Access context is not available.");
101+
return info->getAudience();
101102
}
102103

103-
jsg::Promise<jsg::JsValue> AccessContext::getIdentity(jsg::Lock& js) {
104-
JSG_FAIL_REQUIRE(Error, "Access context is not available.");
104+
jsg::Promise<jsg::Optional<jsg::JsValue>> AccessContext::getIdentity(jsg::Lock& js) {
105+
auto& ioctx = IoContext::current();
106+
return ioctx.awaitIo(js, info->getIdentity(),
107+
[](jsg::Lock& js, kj::Maybe<kj::String> json) -> jsg::Optional<jsg::JsValue> {
108+
KJ_IF_SOME(j, json) {
109+
return jsg::JsValue(js.parseJson(j).getHandle(js));
110+
}
111+
return kj::none;
112+
});
105113
}
106114

107115
jsg::Optional<jsg::Ref<AccessContext>> ExecutionContext::getAccess(jsg::Lock& js) {
108-
// Hook for the embedding application to provide an AccessContext.
109-
// The default Worker::Api implementation returns kj::none.
110-
if (IoContext::hasCurrent()) {
111-
return Worker::Isolate::from(js).getApi().getCtxAccessProperty(js);
116+
// Pull the per-request AccessInfo (if any) off the current IncomingRequest. Standalone workerd
117+
// never supplies one; production embedders construct one before calling newWorkerEntrypoint().
118+
if (!IoContext::hasCurrent()) return kj::none;
119+
auto& ioctx = IoContext::current();
120+
KJ_IF_SOME(info, ioctx.getAccessInfo()) {
121+
return js.alloc<AccessContext>(ioctx.addObject(kj::addRef(info)));
112122
}
113123
return kj::none;
114124
}

src/workerd/api/global-scope.h

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ namespace workerd::jsg {
2525
class DOMException;
2626
} // namespace workerd::jsg
2727

28+
namespace workerd {
29+
class AccessInfo;
30+
} // namespace workerd
31+
2832
namespace workerd::api {
2933

3034
class Tracing;
@@ -240,19 +244,24 @@ class CacheContext: public jsg::Object {
240244
}
241245
};
242246

243-
// Base class for the ctx.access object providing Cloudflare Access authentication context.
244-
// Subclass when embedding to provide an implementation.
247+
// Concrete wrapper exposing per-request Cloudflare Access authentication info to JavaScript
248+
// as `ctx.access`. The actual auth data is supplied by the embedding application via
249+
// `workerd::AccessInfo`, which is plumbed through `newWorkerEntrypoint()` onto
250+
// `IoContext::IncomingRequest`.
251+
//
252+
// Standalone workerd never constructs one of these (no `AccessInfo` is supplied), so
253+
// `ctx.access` is `undefined`. Embedders construct a concrete `AccessInfo` subclass and pass it
254+
// through the entrypoint; `ExecutionContext::getAccess()` lazily wraps it in this class.
245255
class AccessContext: public jsg::Object {
246256
public:
257+
explicit AccessContext(IoOwn<AccessInfo> info): info(kj::mv(info)) {}
258+
247259
// Returns the audience claim from the Access JWT.
248-
//
249-
// The default implementation throws — only meaningful when overridden by the embedding.
250-
virtual kj::StringPtr getAud();
260+
kj::StringPtr getAud();
251261

252-
// Fetches the full identity information for the authenticated user.
253-
//
254-
// The default implementation throws — only meaningful when overridden by the embedding.
255-
virtual jsg::Promise<jsg::JsValue> getIdentity(jsg::Lock& js);
262+
// Fetches the full identity information for the authenticated user. Resolves to `undefined`
263+
// if no identity is associated with the request (e.g. service-token authentication).
264+
jsg::Promise<jsg::Optional<jsg::JsValue>> getIdentity(jsg::Lock& js);
256265

257266
JSG_RESOURCE_TYPE(AccessContext) {
258267
JSG_READONLY_INSTANCE_PROPERTY(aud, getAud);
@@ -262,6 +271,9 @@ class AccessContext: public jsg::Object {
262271
getIdentity(): Promise<CloudflareAccessIdentity | undefined>;
263272
});
264273
}
274+
275+
private:
276+
IoOwn<AccessInfo> info;
265277
};
266278
class ExecutionContext: public jsg::Object {
267279
public:

src/workerd/api/tests/ctx-access-test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { strictEqual } from 'node:assert';
77
export const ctxAccessPropertyExists = {
88
test(controller, env, ctx) {
99
// The access property is always present on ctx as a lazy instance property.
10-
// In standalone workerd (no embedding override), the value is undefined because
11-
// the default Worker::Api::getCtxAccessProperty() returns kj::none.
10+
// In standalone workerd no AccessInfo is supplied to newWorkerEntrypoint(), so the
11+
// current IncomingRequest has no AccessInfo and getAccess() returns kj::none, which
12+
// surfaces as `undefined` to JS.
1213
strictEqual('access' in ctx, true);
1314
strictEqual(ctx.access, undefined);
1415
},

src/workerd/io/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ wd_cc_library(
4747
"worker-fs.c++",
4848
] + ["//src/workerd/api:srcs"],
4949
hdrs = [
50+
"access-info.h",
5051
"compatibility-date.h",
5152
"external-pusher.h",
5253
"hibernation-manager.h",

src/workerd/io/access-info.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2026 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
5+
#pragma once
6+
7+
#include <kj/async.h>
8+
#include <kj/refcount.h>
9+
#include <kj/string.h>
10+
11+
namespace workerd {
12+
13+
// Per-request Cloudflare Access authentication information.
14+
//
15+
// This is the I/O-side carrier for Access auth data. It is created by the embedding application
16+
// (e.g. the production runtime) before invoking the worker, plumbed through `newWorkerEntrypoint()`
17+
// into the `IoContext::IncomingRequest`, and surfaced to JavaScript by the concrete
18+
// `api::AccessContext` wrapper as `ctx.access`.
19+
//
20+
// In standalone workerd this is never constructed; `ctx.access` evaluates to `undefined`.
21+
//
22+
// This type intentionally lives in `io/` rather than `api/` because:
23+
// - It is the polymorphism boundary between embedders (workerd vs. production), not the
24+
// JS-facing type.
25+
// - It carries per-request data that flows through `newWorkerEntrypoint` → `IncomingRequest`,
26+
// not through `Worker::Api` (which is per-isolate) or `IoChannelFactory`.
27+
class AccessInfo: public kj::Refcounted {
28+
public:
29+
virtual ~AccessInfo() noexcept(false) = default;
30+
31+
// The audience claim from the Access JWT. Stable for the lifetime of the request.
32+
virtual kj::StringPtr getAudience() = 0;
33+
34+
// Fetches the full identity information for the authenticated user, equivalent to calling
35+
// /cdn-cgi/access/get-identity. The returned string is a JSON document; `kj::none` indicates
36+
// no identity is available (e.g. service-token authentication).
37+
virtual kj::Promise<kj::Maybe<kj::String>> getIdentity() = 0;
38+
};
39+
40+
} // namespace workerd

src/workerd/io/io-context.c++

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "io-context.h"
66

7+
#include <workerd/io/access-info.h>
78
#include <workerd/io/io-gate.h>
89
#include <workerd/io/tracer.h>
910
#include <workerd/io/worker.h>
@@ -218,11 +219,13 @@ IoContext::IncomingRequest::IoContext_IncomingRequest(kj::Own<IoContext> context
218219
kj::Own<IoChannelFactory> ioChannelFactoryParam,
219220
kj::Own<RequestObserver> metricsParam,
220221
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
222+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
221223
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan)
222224
: context(kj::mv(contextParam)),
223225
metrics(kj::mv(metricsParam)),
224226
workerTracer(kj::mv(workerTracer)),
225227
ioChannelFactory(kj::mv(ioChannelFactoryParam)),
228+
accessInfo(kj::mv(accessInfo)),
226229
maybeTriggerInvocationSpan(kj::mv(maybeTriggerInvocationSpan)) {}
227230

228231
tracing::InvocationSpanContext& IoContext::IncomingRequest::getInvocationSpanContext() {

src/workerd/io/io-context.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "worker.h"
99

1010
#include <workerd/api/deferred-proxy.h>
11+
#include <workerd/io/access-info.h>
1112
#include <workerd/io/actor-id.h>
1213
#include <workerd/io/external-pusher.h>
1314
#include <workerd/io/io-channels.h>
@@ -71,6 +72,7 @@ class IoContext_IncomingRequest final {
7172
kj::Own<IoChannelFactory> ioChannelFactory,
7273
kj::Own<RequestObserver> metrics,
7374
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
75+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
7476
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan);
7577
KJ_DISALLOW_COPY_AND_MOVE(IoContext_IncomingRequest);
7678
~IoContext_IncomingRequest() noexcept(false);
@@ -131,6 +133,12 @@ class IoContext_IncomingRequest final {
131133
return rootUserTraceSpan.addRef();
132134
}
133135

136+
// The Cloudflare Access auth info for this request, if any was provided by the embedder. Used
137+
// to populate `ctx.access` in JavaScript.
138+
kj::Maybe<AccessInfo&> getAccessInfo() {
139+
return accessInfo.map([](kj::Own<AccessInfo>& p) -> AccessInfo& { return *p; });
140+
}
141+
134142
// The invocation span context is a unique identifier for a specific
135143
// worker invocation.
136144
tracing::InvocationSpanContext& getInvocationSpanContext();
@@ -140,6 +148,7 @@ class IoContext_IncomingRequest final {
140148
kj::Own<RequestObserver> metrics;
141149
kj::Maybe<kj::Own<BaseTracer>> workerTracer;
142150
kj::Own<IoChannelFactory> ioChannelFactory;
151+
kj::Maybe<kj::Own<AccessInfo>> accessInfo;
143152

144153
// Root user trace span for this request. Populated during delivered() via
145154
// BaseTracer::makeUserRequestSpan(); otherwise a null SpanParent. The tracer it references
@@ -240,6 +249,14 @@ class IoContext final: public kj::Refcounted, private kj::TaskSet::ErrorHandler
240249
return getCurrentIncomingRequest().getRootUserTraceSpan();
241250
}
242251

252+
// The Cloudflare Access auth info for the current incoming request, if any was provided by
253+
// the embedder. Used to populate `ctx.access` in JavaScript.
254+
kj::Maybe<AccessInfo&> getAccessInfo() {
255+
if (incomingRequests.empty()) return kj::none;
256+
return getCurrentIncomingRequest().getAccessInfo();
257+
}
258+
259+
243260
LimitEnforcer& getLimitEnforcer() {
244261
return *limitEnforcer;
245262
}

src/workerd/io/worker-entrypoint.c++

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <workerd/api/basics.h>
88
#include <workerd/api/global-scope.h>
99
#include <workerd/api/util.h>
10+
#include <workerd/io/access-info.h>
1011
#include <workerd/io/features.h>
1112
#include <workerd/io/io-context.h>
1213
#include <workerd/io/limit-enforcer.h>
@@ -60,6 +61,7 @@ class WorkerEntrypoint final: public WorkerInterface {
6061
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
6162
kj::Maybe<kj::String> cfBlobJson,
6263
kj::Maybe<Worker::VersionInfo> versionInfo,
64+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
6365
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan,
6466
bool isDynamicDispatch);
6567

@@ -110,6 +112,7 @@ class WorkerEntrypoint final: public WorkerInterface {
110112
kj::Own<IoChannelFactory> ioChannelFactory,
111113
kj::Own<RequestObserver> metrics,
112114
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
115+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
113116
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan);
114117

115118
template <typename T>
@@ -183,6 +186,7 @@ kj::Own<WorkerInterface> WorkerEntrypoint::construct(ThreadContext& threadContex
183186
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
184187
kj::Maybe<kj::String> cfBlobJson,
185188
kj::Maybe<Worker::VersionInfo> versionInfo,
189+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
186190
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan,
187191
bool isDynamicDispatch) {
188192
TRACE_EVENT("workerd", "WorkerEntrypoint::construct()");
@@ -191,7 +195,7 @@ kj::Own<WorkerInterface> WorkerEntrypoint::construct(ThreadContext& threadContex
191195
waitUntilTasks, tunnelExceptions, isDynamicDispatch, entrypointName, kj::mv(props),
192196
kj::mv(cfBlobJson), kj::mv(versionInfo));
193197
obj->init(kj::mv(worker), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency),
194-
kj::mv(ioChannelFactory), kj::addRef(*metrics), kj::mv(workerTracer),
198+
kj::mv(ioChannelFactory), kj::addRef(*metrics), kj::mv(workerTracer), kj::mv(accessInfo),
195199
kj::mv(maybeTriggerInvocationSpan));
196200
auto& wrapper = metrics->wrapWorkerInterface(*obj);
197201
return kj::attachRef(wrapper, kj::mv(obj), kj::mv(metrics));
@@ -222,6 +226,7 @@ void WorkerEntrypoint::init(kj::Own<const Worker> worker,
222226
kj::Own<IoChannelFactory> ioChannelFactory,
223227
kj::Own<RequestObserver> metrics,
224228
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
229+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
225230
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan) {
226231
TRACE_EVENT("workerd", "WorkerEntrypoint::init()");
227232
// We need to construct the IoContext -- unless this is an actor and it already has a
@@ -252,7 +257,8 @@ void WorkerEntrypoint::init(kj::Own<const Worker> worker,
252257
}
253258

254259
incomingRequest = kj::heap<IoContext::IncomingRequest>(kj::mv(context), kj::mv(ioChannelFactory),
255-
kj::mv(metrics), kj::mv(workerTracer), kj::mv(maybeTriggerInvocationSpan))
260+
kj::mv(metrics), kj::mv(workerTracer), kj::mv(accessInfo),
261+
kj::mv(maybeTriggerInvocationSpan))
256262
.attach(kj::mv(actor));
257263
}
258264

@@ -1007,12 +1013,13 @@ kj::Own<WorkerInterface> newWorkerEntrypoint(ThreadContext& threadContext,
10071013
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
10081014
kj::Maybe<kj::String> cfBlobJson,
10091015
kj::Maybe<Worker::VersionInfo> versionInfo,
1016+
kj::Maybe<kj::Own<AccessInfo>> accessInfo,
10101017
kj::Maybe<tracing::InvocationSpanContext> maybeTriggerInvocationSpan,
10111018
bool isDynamicDispatch) {
10121019
return WorkerEntrypoint::construct(threadContext, kj::mv(worker), kj::mv(entrypointName),
10131020
kj::mv(props), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency),
10141021
kj::mv(ioChannelFactory), kj::mv(metrics), waitUntilTasks, tunnelExceptions,
1015-
kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(versionInfo),
1022+
kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(versionInfo), kj::mv(accessInfo),
10161023
kj::mv(maybeTriggerInvocationSpan), isDynamicDispatch);
10171024
}
10181025

src/workerd/io/worker-entrypoint.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#pragma once
66

7+
#include <workerd/io/access-info.h>
78
#include <workerd/io/frankenvalue.h>
89
#include <workerd/io/worker.h>
910

@@ -42,6 +43,9 @@ kj::Own<WorkerInterface> newWorkerEntrypoint(ThreadContext& threadContext,
4243
kj::Maybe<kj::Own<BaseTracer>> workerTracer,
4344
kj::Maybe<kj::String> cfBlobJson,
4445
kj::Maybe<Worker::VersionInfo> versionInfo,
46+
// Per-request Cloudflare Access info. Supplied by the embedding application; standalone
47+
// workerd passes kj::none, which causes `ctx.access` to be `undefined` in JS.
48+
kj::Maybe<kj::Own<AccessInfo>> accessInfo = kj::none,
4549
// The trigger invocation span may be propagated from other request. If it is provided,
4650
// the implication is that this worker entrypoint is being created as a subrequest or
4751
// subtask of another request. If it is kj::none, then this invocation is a top-level

src/workerd/io/worker.c++

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,10 +526,6 @@ jsg::Optional<jsg::Ref<api::CacheContext>> Worker::Api::getCtxCacheProperty(jsg:
526526
return kj::none;
527527
}
528528

529-
jsg::Optional<jsg::Ref<api::AccessContext>> Worker::Api::getCtxAccessProperty(jsg::Lock& js) const {
530-
return kj::none;
531-
}
532-
533529
struct Worker::Impl {
534530
kj::Maybe<jsg::JsContext<api::ServiceWorkerGlobalScope>> context;
535531

0 commit comments

Comments
 (0)