Goal
Add Okta authentication to gate /intake/* for testing, enforced server-side in the Cloudflare Worker (workers/website/). Uses the existing confidential Okta Web app (client ID + secret) via the OIDC Authorization Code flow.
Why the worker (and not the .aem.page URL)
The originally requested URL stage--drago-toolkit--adobedevxsc.aem.page/intake/form is on Adobe's hostname and cannot be fronted by Cloudflare, so the worker can't intercept it. A confidential client (secret) also can't be used safely in browser-only code. Therefore auth is enforced in the worker, which is pointed at the stage AEM branch and tested via wrangler dev (http://localhost:8787) and/or a *.workers.dev deploy.
Trade-off accepted: testing happens through the worker host, not the literal .aem.page stage URL.
Scope / behavior
- Gate all of
/intake/*; everything else stays public.
- Any authenticated Okta user is allowed (no group/email restriction).
- For testing, point the worker origin at stage:
AEM_ORG=adobedevxsc, AEM_SITE=drago-toolkit, AEM_BRANCH=stage.
Implementation
workers/website/handlers/auth.js (new) — OIDC Authorization Code flow with Web Crypto (no heavy deps):
requireAuth() — allow non-/intake/ and /auth/* paths; allow valid session; else 302 to Okta /v1/authorize with client_id, redirect_uri=<origin>/auth/callback, response_type=code, scope=openid profile email, random state + nonce (short-lived signed cookie carrying original path).
handleCallback() — validate state, exchange code at /v1/token using client_id + client_secret (server-side), verify id_token (RS256 via cached JWKS, iss/aud/exp/nonce), set signed session cookie, redirect to saved path.
handleLogout() — clear session cookie.
- Session cookie: HMAC-signed (
SESSION_SECRET), HttpOnly; Secure; SameSite=Lax, ~1h TTL.
workers/website/index.js — handle /auth/callback + /auth/logout, then call requireAuth() before the existing ROUTES dispatch.
workers/website/index.js (formatRequest) — add AEM_BRANCH var (default main) so origin = ${AEM_BRANCH}--${AEM_SITE}--${AEM_ORG}.aem.live.
- Secrets/config
workers/website/.dev.vars (new, gitignored): OKTA_ISSUER, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET, SESSION_SECRET.
wrangler.toml: add AEM_BRANCH; document Okta values + SESSION_SECRET set via wrangler secret put for deployed envs.
.gitignore: add .dev.vars and .env.
Okta app configuration (manual)
- Sign-in redirect URI:
http://localhost:8787/auth/callback (+ *.workers.dev callback if deployed).
- Sign-out redirect URI:
http://localhost:8787/.
- Grant type: Authorization Code. Assignment: everyone in the org.
Test plan
cd workers/website && npx wrangler dev
http://localhost:8787/intake/form → redirect to Okta → log in → returns to form. /intake/* all gated.
http://localhost:8787/ and other paths → public, no redirect.
- Verify: session cookie
HttpOnly/Secure/SameSite=Lax; logout clears it; state/nonce mismatch rejected.
Security notes
CSRF via state, replay protection via nonce, full id_token validation, signed short-lived session cookie, secret only in worker env, no token logging.
Blocking prerequisite
Goal
Add Okta authentication to gate
/intake/*for testing, enforced server-side in the Cloudflare Worker (workers/website/). Uses the existing confidential Okta Web app (client ID + secret) via the OIDC Authorization Code flow.Why the worker (and not the
.aem.pageURL)The originally requested URL
stage--drago-toolkit--adobedevxsc.aem.page/intake/formis on Adobe's hostname and cannot be fronted by Cloudflare, so the worker can't intercept it. A confidential client (secret) also can't be used safely in browser-only code. Therefore auth is enforced in the worker, which is pointed at the stage AEM branch and tested viawrangler dev(http://localhost:8787) and/or a*.workers.devdeploy.Trade-off accepted: testing happens through the worker host, not the literal
.aem.pagestage URL.Scope / behavior
/intake/*; everything else stays public.AEM_ORG=adobedevxsc,AEM_SITE=drago-toolkit,AEM_BRANCH=stage.Implementation
workers/website/handlers/auth.js(new) — OIDC Authorization Code flow with Web Crypto (no heavy deps):requireAuth()— allow non-/intake/and/auth/*paths; allow valid session; else 302 to Okta/v1/authorizewithclient_id,redirect_uri=<origin>/auth/callback,response_type=code,scope=openid profile email, randomstate+nonce(short-lived signed cookie carrying original path).handleCallback()— validatestate, exchangecodeat/v1/tokenusingclient_id+client_secret(server-side), verifyid_token(RS256 via cached JWKS,iss/aud/exp/nonce), set signed session cookie, redirect to saved path.handleLogout()— clear session cookie.SESSION_SECRET),HttpOnly; Secure; SameSite=Lax, ~1h TTL.workers/website/index.js— handle/auth/callback+/auth/logout, then callrequireAuth()before the existingROUTESdispatch.workers/website/index.js(formatRequest) — addAEM_BRANCHvar (defaultmain) so origin =${AEM_BRANCH}--${AEM_SITE}--${AEM_ORG}.aem.live.workers/website/.dev.vars(new, gitignored):OKTA_ISSUER,OKTA_CLIENT_ID,OKTA_CLIENT_SECRET,SESSION_SECRET.wrangler.toml: addAEM_BRANCH; document Okta values +SESSION_SECRETset viawrangler secret putfor deployed envs..gitignore: add.dev.varsand.env.Okta app configuration (manual)
http://localhost:8787/auth/callback(+*.workers.devcallback if deployed).http://localhost:8787/.Test plan
cd workers/website && npx wrangler devhttp://localhost:8787/intake/form→ redirect to Okta → log in → returns to form./intake/*all gated.http://localhost:8787/and other paths → public, no redirect.HttpOnly/Secure/SameSite=Lax; logout clears it;state/noncemismatch rejected.Security notes
CSRF via
state, replay protection vianonce, fullid_tokenvalidation, signed short-lived session cookie, secret only in worker env, no token logging.Blocking prerequisite
https://<org>.okta.comorhttps://<org>.okta.com/oauth2/default). Client ID, client secret, andkidare on hand; the issuer is required to resolve the/authorize,/token, and JWKS endpoints.