You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0111e24
Browse filesBrowse the repository at this point in the historyBrowse files
Retire Cloudflare Access: demo on Google + email codes, cf-access deprecated
Demo: `/auth/sso` and the `ACCESS_*` config are gone. Staff sign in with
Google like everyone else, and `POST /api/staff` promotes a view session at
`STAFF_DOMAIN` to the admin gate, so one registered redirect URI serves both.
Copy no longer offers Access/SSO.
Library: README leads with Google/OIDC + email codes; the Access adapter moves
to a "Legacy" note and its exports are `@deprecated` — kept until gcs and
cw-s3 (pinned to older SHAs, still importing it) finish migrating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyJyzSYpRxJuL6x6NyjpG3
Copy file name to clipboardExpand all lines: README.md
+4-14Lines changed: 4 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -104,15 +104,7 @@ if (res.ok) return new Response(null, { headers: { 'set-cookie': res.cookie } })
104
104
105
105
Every knob is optional; zero-config is an unlimited-use, never-expiring, unnamed link. `maxRedeems` counts **sessions minted** (≈ distinct browsers), not requests — which is what makes "one-use link" mean what a human predicts. Note that `maxRedeems: 1` is hostile UX in practice (the recipient opens it on their phone, then their laptop, and is locked out); prefer unlimited-redeem, named, logged, and revocable.
106
106
107
-
**SSO.** Point one CF Access application at `/auth/sso` and leave the rest of the site public at the edge:
**Or skip Access entirely.**`@open-athena/auth/oidc` signs people in against an OIDC provider directly, so the hosted chooser and its generic copy are replaced by a page you own:
107
+
**Sign in with Google** (or any OIDC issuer). `@open-athena/auth/oidc` signs people in against the provider directly — no Cloudflare Access, no Zero Trust seats, and a sign-in page you own. Pair it with One Tap (`GoogleOneTap`) in the page and email codes (below) for addresses Google can't vouch for:
A verified address that policy rejects redirects with `?denied=<email>` rather than 403ing, which is what lets an app pre-fill request-access with an address the *provider* vouched for instead of one the visitor typed.
141
133
142
-
There's also a seat argument: every Access-authenticated user consumes a Cloudflare Zero Trust seat, while share links never touch Access at all. A growing allowlist hits that ceiling; this is the way off it.
143
-
144
-
`ssoSessionHandler` is the same thing for a deployment that can mint sessions but not verify them — the auth store lives in another worker, so there's no gate to hand it. It takes `{ secret, teamDomain, aud, cookieName }` and mints for any Access-verified email; the gate that later verifies the cookie re-derives scopes from `policy` on every request, so authorization isn't being skipped, just deferred to where it can be answered.
134
+
**Legacy: Cloudflare Access** (`@open-athena/auth/cf-access`). The package began as a layer *on* Access — Access authenticated, the gate added share links and the log — and the adapter still exists for deployments that haven't cut over: `ssoHandler` trades an Access JWT on one Access-gated path (`/auth/sso`) for a gate session, and `ssoSessionHandler` does the same where the verifying gate lives in another worker. It's deprecated in favour of the OIDC + email-code path above, which does the same job without Access: every Access-authenticated user consumes a Zero Trust seat (the free tier caps at 50), and the hosted chooser can't be styled. It will be removed once its last consumers have migrated.
145
135
146
136
**Revocation is instant.** Grant-backed sessions re-join their grant row on every request, so `gate.revoke(id)` kills every session that link ever minted — no waiting out a cookie TTL. That property is what makes the social story work: assume links get forwarded, and design so forwarding is *visible and revocable* rather than prevented.
147
137
@@ -257,8 +247,8 @@ The default read is the Cloud Identity API, which is what honours a group-*owner
257
247
258
248
```tsx
259
249
<AuthGate
260
-
source={{ kind: 'app' }} // or { kind: 'edge' } for Tier 1 — the only line that changes
Copy file name to clipboardExpand all lines: demo/README.md
+6-7Lines changed: 6 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ Three pages:
6
6
7
7
| Path | What it shows |
8
8
|---|---|
9
-
|`/`| The ways in: the library's `SignInPanel` (Google One Tap + redirect, Access SSO, an emailed code), and — more prominently — two share links you can just click |
9
+
|`/`| The ways in: the library's `SignInPanel` (Google One Tap + redirect, an emailed code / magic link), and — more prominently — two share links you can just click |
10
10
|`/dashboard`| The gated page. The wall in front of it, then a page whose only content is what the gate knows about you |
11
11
|`/admin`| The power-user side, in a throwaway sandbox: mint links, watch their log, disable / rotate / revoke them, edit an allowlist and run a (simulated) directory sync |
12
12
@@ -27,15 +27,15 @@ Step 5 is the load-bearing one: grant-backed sessions re-join their grant row on
27
27
**Simulated, and labelled as such in the page:**
28
28
29
29
-**Mail.**`emailCodeAuth`'s `start` reply never says whether mail went out (so the form can't probe who's allowed), and delivery *is* the verification. This demo has no sending domain for most visitors, so its `send` captures the message instead of mailing it, stashes it in a short-lived `HttpOnly` cookie only your browser can read back (`GET /auth/email/outbox`), and the form shows you the code and link. That proves nothing about the address, and the page says so. Mail really goes out only when `RESEND_API_KEY` + `MAIL_FROM` are set *and* the recipient's domain is in `MAIL_DOMAINS` — a public form that mails whatever address a stranger types is a spam cannon pointed at other people.
30
-
-**Google.** Without `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, `/auth/google/*` answers 503, so the page drops One Tap and the Google button (and says why) rather than offering a sign-in that can't complete. The staff SSO button is likewise hidden under `pnpm dev`, where no Cloudflare Access sits in front of `/auth/sso`. Google exposes no API to create the client; `scripts/provision-oauth-client.mjs` walks the one manual step.
30
+
-**Google.** Without `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, `/auth/google/*` answers 503, so the page drops One Tap and the Google button (and says why) rather than offering a sign-in that can't complete. Google exposes no API to create the client; `scripts/provision-oauth-client.mjs` walks the one manual step.
31
31
-**The directory sync.**`Admin → Allowlist → Sync now``replaceSource`s `sync:staff@example.org` with a random subset of a five-person roster, returning the same `[{ group, source, count }]` shape `syncGroupsToAllowlist` returns. A real deployment puts that call at the marked spot in `functions/api/admin/[[path]].ts`, with a service-account key. The table isn't wired into any policy here (the demo admits any address) — which demonstrates a real property: mounting the editor never changes who gets in; `allowlistPolicy(store)` in the gate's `policy` is the separate line that does.
32
32
33
33
## Two gates, one grants table
34
34
35
35
`functions/_lib/gates.ts` builds two gates over the same D1 store:
36
36
37
-
-**`viewGate`** (`oa_demo_view`) — guards the dashboard. Its policy admits staff and *anyone who isn't a sandbox identity*: an email session (SSO, Google, or an emailed code — they all end in `gate.signIn`) re-derives its scopes from this policy on every request, so "who may sign in by email" is decided in that one line. A real deployment writes `domainPolicy`, `allowlistPolicy`, or both there.
38
-
-**`adminGate`** (`oa_demo_admin`) — guards the admin page. Staff get `admin` + `requests` + `reports`; a sandbox identity gets `admin` only, so a visitor playing admin still meets the wall on the dashboard and has to mint themselves a link.
37
+
-**`viewGate`** (`oa_demo_view`) — guards the dashboard. Its policy admits staff and *anyone who isn't a sandbox identity*: an email session (Google or an emailed code — both end in `gate.signIn`) re-derives its scopes from this policy on every request, so "who may sign in by email" is decided in that one line. A real deployment writes `domainPolicy`, `allowlistPolicy`, or both there.
38
+
-**`adminGate`** (`oa_demo_admin`) — guards the admin page. Staff (Google sign-ins at `STAFF_DOMAIN`, promoted from their view session by `POST /api/staff` — one registered redirect URI serves both gates) get `admin` + `requests` + `reports`; a sandbox identity gets `admin` only, so a visitor playing admin still meets the wall on the dashboard and has to mint themselves a link.
39
39
40
40
Separate cookie names let one browser hold both roles at once. The shared store is what makes revocation in one visible to the other.
41
41
@@ -63,18 +63,17 @@ Migrations come straight from the package (`migrations_dir = "../migrations"`),
63
63
wrangler d1 create oa-auth-demo # paste database_id into wrangler.toml
wrangler pages secret put SESSION_SECRET # openssl rand -base64 32
66
-
wrangler pages secret put ACCESS_AUD # the Access application's AUD tag
67
66
pnpm deploy
68
67
```
69
68
70
-
Then, for SSO, create **one**Cloudflare Access application covering `auth.oa.dev/auth/sso` and nothing else. Narrowness is the point: the landing page and its og:image stay publicly crawlable so unfurls work, and Access is reduced to an IdP on a single path. Set `ACCESS_TEAM_DOMAIN` in `wrangler.toml` to your Zero Trust team domain.
69
+
No Cloudflare Access / Zero Trust anywhere: the whole site is public at the edge, and the gate is the app's.
71
70
72
71
Optional, each turning a simulated thing real (see `wrangler.toml` for the full list):
73
72
74
73
-`RESEND_API_KEY` + `MAIL_FROM` (`scripts/provision-resend.mjs` stores both) and `MAIL_DOMAINS` — email codes are mailed to those domains.
75
74
-`GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET` (`scripts/provision-oauth-client.mjs`) — Google sign-in and One Tap work.
76
75
77
-
Without any of them, `/auth/sso` returns 401, `/auth/google/*` returns 503, and codes show on screen — everything else (share links, request-access, the log, the allowlist) still works.
76
+
Without any of them, `/auth/google/*` returns 503, and codes show on screen — everything else (share links, request-access, the log, the allowlist) still works.
<title>@open-athena/auth — share links, SSO, and an access log</title>
6
+
<title>@open-athena/auth — share links, Google sign-in, and an access log</title>
7
7
<meta
8
8
name="description"
9
9
content="Live demo: sign in with Google or an emailed code, or open a share link; mint your own, watch what the recipient did, revoke it and see the session die."
content="Named share links, SSO, email codes, and an access log for gated dashboards. Try it: open a link, mint your own, watch the log, revoke it."
14
+
content="Named share links, Google sign-in, email codes, and an access log for gated dashboards. Try it: open a link, mint your own, watch the log, revoke it."
0 commit comments