Today: the browser can obtain power
The storefront can use exposed client credentials to obtain a trusted token. If that token is copied, another caller can present it directly to customer APIs.
A practical guide to brokering SAP Commerce Cloud access with a Cloudflare Worker: server-side credentials, session handles, per-request authorization, and an authenticated origin path.
Receives an opaque session handle. It does not receive the SAP client secret or SAP access token.
Verifies session context, checks ownership and operation, stores the SAP token, and injects it server-side.
Maps the handle to expiry, registered customer or guest-cart context, and the origin token reference.
Accepts the real token only over the approved backend path. Alternate access paths are authenticated or denied.
Target design: browser → session handle → Worker authorization → server-side SAP token.
The application keeps its shopping behavior, but privileged custody and authorization move to the server side.
The storefront can use exposed client credentials to obtain a trusted token. If that token is copied, another caller can present it directly to customer APIs.
The Worker holds the secret and origin token. The browser presents a session handle that SAP does not accept directly.
Before proxying, the Worker resolves who the session belongs to and whether the requested customer object and operation are allowed.
Each primitive answers one design question. Do not treat credential custody as a substitute for authorization.
Return an opaque handle as an OAuth-shaped bearer value for minimal frontend change, or as a Secure, HttpOnly, SameSite cookie for stronger JavaScript isolation.
Tradeoff: bearer handles are easier to retrofit; cookies require CORS, cookie-scope, and CSRF design.
Registered sessions need a trustworthy customer identity. Guest sessions need a server-established cart/checkout context.
Important: Turnstile can reduce automated session creation, but it is not customer authentication or proof of object ownership.
The Worker uses its secret to call the SAP token endpoint. The resulting SAP token is stored server-side and never returned to browser-accessible traffic.
Compatibility: the SAP token can be opaque or JWT because the browser no longer needs to present it.
Map a high-entropy handle to the customer or guest context, allowed scopes, expiry, revocation state, and origin-token reference. Use Workers KV for simple shared state or Durable Objects for stronger coordination.
Reminder: a copied handle remains a credential until it expires or is revoked.
Resolve the handle, then check the requested customer ID, cart ID, operation, and method against the session’s permissions.
Critical: a Worker that blindly swaps every valid handle for a trusted SAP token only improves custody; it does not stop Alice from requesting Bob’s data.
After authorization, the Worker adds the real SAP token to the outbound request and forwards the full method, path, query, body, and required headers.
Coverage: test reads and writes—GET, POST, PUT, PATCH, and DELETE—not only the initial account lookup.
Rotate the exposed SAP secret, address previously issued tokens, and restrict privileged token issuance and API access to the approved Worker/backend lanes using SAP-supported authentication.
Caution: a broad Cloudflare IP allowlist does not uniquely identify one Worker. Backend integrations need their own authenticated lanes.
Use two customer accounts and guests to test normal shopping, unauthorized reads and writes, expired/revoked handles, alternate origins, preflight requests, and every relevant HTTP method.
Exit condition: legitimate journeys pass and cross-customer access is denied before SAP processes the request.
Keep the API surface small and explicit. The browser should not receive or present SAP credentials.
| Step | Browser sends | Worker validates | Worker returns / forwards |
|---|---|---|---|
| Session bootstrap | Login/guest proof and optional abuse challenge. | Registered customer identity or guest-cart context, challenge result, and issuance policy. | An OAuth-shaped response whose access_token is sess_…, or an HttpOnly session cookie. |
| Customer API call | Authorization: Bearer sess_… or the session cookie. |
Session state, expiry, revocation, object ownership, method, and allowed operation. | Allowed request proxied to SAP with the real server-side SAP token. |
| Denied request | A session handle for a resource or operation it does not own. | Ownership and operation policy. | 403 before the SAP token is attached or the origin is called. |
Use this as a design walkthrough. Replace the placeholder identity checks with Kao’s real registered and guest authorization model.
const SESSION_TTL_SECONDS = 3600;
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/bff/session") {
return createSession(request, env);
}
if (url.pathname.startsWith("/api/")) {
return proxyAuthorizedRequest(request, env);
}
return json({ error: "not_found" }, 404);
},
};
async function createSession(request, env) {
// 1. Validate registered login or server-established guest context.
// 2. Optionally verify Turnstile as an abuse gate.
// 3. Mint or reuse the SAP token server-side.
const sessionId = `sess_${crypto.randomUUID()}`;
const session = {
customerId: "replace-with-verified-customer-id",
guestCartIds: [],
originAccessToken: await getOriginToken(env),
expiresAt: Date.now() + SESSION_TTL_SECONDS * 1000,
revoked: false,
};
await env.SESSIONS.put(sessionId, JSON.stringify(session), {
expirationTtl: SESSION_TTL_SECONDS,
});
return json({
access_token: sessionId,
token_type: "bearer",
expires_in: SESSION_TTL_SECONDS,
});
}
async function proxyAuthorizedRequest(request, env) {
const match = (request.headers.get("authorization") || "")
.match(/^Bearer\s+(sess_[0-9a-fA-F-]+)$/);
if (!match) return json({ error: "missing_session" }, 401);
const stored = await env.SESSIONS.get(match[1], "json");
if (!stored || stored.revoked || stored.expiresAt <= Date.now()) {
return json({ error: "invalid_session" }, 401);
}
if (!isAllowed(request, new URL(request.url), stored)) {
return json({ error: "forbidden" }, 403);
}
const headers = new Headers(request.headers);
headers.set("authorization", `Bearer ${stored.originAccessToken}`);
headers.delete("cookie");
return env.ORIGIN.fetch(new Request(request.url, {
method: request.method,
headers,
body: request.body,
}));
}
function isAllowed(request, url, session) {
// Replace with Kao's ownership policy.
// Registered example: /api/users/{customerId}/...
const userMatch = url.pathname.match(/^\/api\/users\/([^/]+)/);
if (userMatch && decodeURIComponent(userMatch[1]) !== session.customerId) {
return false;
}
// Guest example: allow only cart IDs established for this guest session.
const cartMatch = url.pathname.match(/^\/api\/carts\/([^/]+)/);
if (cartMatch && !session.guestCartIds.includes(decodeURIComponent(cartMatch[1]))) {
return false;
}
return true;
}
name = "kao-sap-bff"
main = "src/worker.js"
compatibility_date = "2026-10-06"
routes = [
{ pattern = "api.example.com/*", zone_name = "example.com" }
]
[[kv_namespaces]]
binding = "SESSIONS"
id = ""
[[services]]
binding = "ORIGIN"
service = ""
# Set secrets outside source control:
# wrangler secret put SAP_CLIENT_ID
# wrangler secret put SAP_CLIENT_SECRET
# wrangler secret put TURNSTILE_SECRET
Complete these items before calling the pattern protective.
Use two controlled customers and a guest. The goal is not just hiding a token; it is preventing cross-customer access.
| Test | Required result |
|---|---|
| Registered and guest shopping | Login, account, cart, checkout, payment, and account-management journeys pass. |
| Alice requests Bob’s resources | Denied before SAP is called; target reads and writes do not occur. |
| Guest requests another cart | Denied unless the cart was established for that guest session. |
| Expired or revoked handle | Rejected; no server-side token substitution occurs. |
| Copied handle | Replay is bounded by policy, expiry, and revocation; suspicious use is observable. |
| Direct or alternate SAP path | Cannot bypass the Worker’s authorization decision with the privileged token lane. |