Skip to content

Inherit collab identity from Cloudflare Access - #148

Merged
molefrog merged 10 commits into
codex/collabfrom
claude/moi-cloudflare-identity-inherit-7w5lxk
Sep 28, 2026
Merged

molefrog merged 10 commits into
codex/collabfrom
claude/moi-cloudflare-identity-inherit-7w5lxk

Conversation

@molefrog

@molefrog molefrog commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #147. A moi deployment behind Cloudflare Access (Zero Trust) can now be configured globally to inherit each viewer's identity, so presence shows who is who without dev profiles. The id and email come from Access's verified token. Tokens carry no display name, so the profile has no name and labels show the email (the nameless users from #147). The color is a stable pick from the persona palette, hashed from the id.

// config.json in moi's data dir, or MOI_CLOUDFLARE_ACCESS_TEAM_DOMAIN + MOI_CLOUDFLARE_ACCESS_AUD
{ "cloudflareAccess": { "teamDomain": "acme", "audience": "<AUD tag>" } }
GET /api/identity    (Cf-Access-Jwt-Assertion: <token>)
{"provider":"cloudflare-access","identity":{"id":"7335d417-…","color":"#10b981","email":"alex@acme.dev"}}

The app loads /api/identity before mounting. The inherited profile replaces a saved dev profile, and /dev/collab shows it instead of the form. Setup and details: Cloudflare Access identity.

Review focus

  • Token verification (server/collab/cloudflare-access.ts): jose (jwtVerify + createRemoteJWKSet, as in Cloudflare's own examples) against <team>/cdn-cgi/access/certs: RS256 only, iss, aud, required exp, 60 s clock tolerance. jose caches keys for ten minutes and refetches for an unknown kid after a 30 s cooldown; failed key fetches are logged and verify nothing. Service tokens (empty sub) resolve to no identity. The CF_Authorization cookie is used only when the header is absent. jose is a new direct dependency (already in the tree through the MCP SDK).
  • Socket pinning (manager.ts, web.ts): with Access configured, the presence upgrade returns 401 without a valid token. join/identity messages must carry the verified id and get the verified profile; a different id, such as an outer host's own identity, is refused with identity_mismatch rather than rewritten.
  • Precedence (identity.ts): an outer host beats Access, which beats a dev profile, whatever the load order. An unverified request signs the tab out. Access supplies no membership, so directories keep the live fallback.

Verification

bun test on the latest codex/collab: 1,931 passed, 4 skipped, 0 failed. Lint, format, registry, typecheck, and a frozen-lockfile install are clean. Manual check: ran the server with Access configured, a mocked certs endpoint, and signed tokens for two users. Header and cookie tokens resolved to the profile; a forged signature or a missing token resolved to identity: null, with Cache-Control: private, no-store. On the presence socket, a join with the verified id but a spoofed name came back as the verified profile, another id was refused with identity_mismatch, and a tokenless upgrade was refused. Two Playwright sessions holding the Access cookie saw each other in the workspace header (alex@acme.dev (you), sam.lee@acme.dev · Overview).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

A deployment behind Cloudflare Access (Zero Trust) can set
`cloudflareAccess` in config.json, or MOI_CLOUDFLARE_ACCESS_TEAM_DOMAIN and
MOI_CLOUDFLARE_ACCESS_AUD, so every viewer inherits their Access identity
instead of creating a dev profile.

The server verifies the Access application token (Cf-Access-Jwt-Assertion,
or the CF_Authorization cookie when a proxy drops the header): RS256
signature against the team's published keys, issuer, audience, and expiry.
The profile takes id from `sub` and email from `email`; tokens carry no
display name, so the name is the email's local part, and the color is a
stable pick from the persona palette hashed from the id.

GET /api/identity serves the verified profile and loads before the app
mounts. The browser uses it over a saved dev profile, and an outer host
still wins. Presence sockets verify the token on upgrade (401 without one)
and pin every join and identity update to the verified profile, so one
viewer cannot appear as another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T11:10:24.782740Z 47075a1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47075a1467

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/collab/manager.ts Outdated
Comment thread server/collab/cloudflare-access.ts Outdated
The base now allows users without names, with built-in labels falling
back to the email. Access tokens carry no display name, so the inherited
profile keeps only id, email, and the id-derived color instead of
deriving a name from the email. The /dev/collab notice labels the
profile with userDisplayName, which also fixes the typecheck against
the optional name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
Fold the verifier rejections into one table, drop cases that repeat
other coverage (concurrent fetches, clock drift, profile helpers), and
merge the config, endpoint, socket pinning, and identity precedence
checks into fewer tests. Coverage stays on signature, issuer, audience,
expiry, key refresh throttling, header and cookie tokens, config
parsing, socket pinning, and source precedence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
A presence socket verified by Cloudflare Access now joins only with the
verified id. A different id, such as an outer host's own identity, is
refused with identity_mismatch instead of being rewritten to the Access
user, so peers never see a different person than the browser shows.

Cached signing keys now expire after ten minutes, so a key Cloudflare
stops publishing stops verifying tokens even when no new key id arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
Replace the hand-rolled JWT parsing, WebCrypto verification, and key
cache with jose's jwtVerify and createRemoteJWKSet, the library
Cloudflare's own verification examples use. Tokens must be RS256, name
the team domain as issuer and one of the configured AUD tags, and carry
an expiry, with 60 seconds of clock tolerance. jose caches the team's
keys for ten minutes and refetches for an unknown key id after a 30
second cooldown. Failed key fetches are logged; bad tokens are not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
The not-yet-valid case set nbf 61 seconds past a timestamp taken at
module load, one second outside the verifier's 60 second tolerance, so
it failed whenever key generation in beforeAll took over a second, as
on a slow CI runner. Expired and not-yet-valid tokens now sit ten
minutes outside the tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
Resolve app-config conflicts: keep the new experimental startup flags
alongside the Cloudflare Access deployment config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK
@molefrog
molefrog merged commit 1c52199 into codex/collab Sep 28, 2026
1 check passed
@molefrog
molefrog deleted the claude/moi-cloudflare-identity-inherit-7w5lxk branch September 28, 2026 15:39
tonyfresher added a commit that referenced this pull request Oct 2, 2026
* Add collab protocol and transactional shared storage

* Add workspace collab workers and explicit CLI setup

* Add collab client state and external identity bridge

* Add collaborative applet primitives and dev identity setup

* Integrate collab with workspace views and personal navigation

* Remember everyone who joins a workspace in a people directory

A person's id now resolves to a name and face after they leave. The
collab worker keeps a `people` table (schema v2, migrated from v1),
upserts it on join and identity change, sends it in `welcome.people`,
and announces new or changed profiles with a `people` message. The
client store merges the directory with live participants.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add id-based people components on a swappable collab backend

Applets refer to a person by id (`id`, or `ids` for several) and the
components resolve name, face, and presence themselves: `Person` (with
`avatarOnly` and `showStatus`), `Facepile`, `Cursor`, `PresenceFrame`,
`PresenceGutter`, and the `usePerson` hook. `PresenceFrame`, and with it
`PresenceField` and `Selection`, hugs the single element it wraps and
copies its corner radius. `SyncStatus` is gone; applets render
`isSaving` and `error` from the shared-state hooks.

Hooks and components now talk to a `CollabBackend` contract from
context instead of the socket client. The workspace provides the live
backend; `createFakeBackend` is an in-memory room behind the same store.
`/dev/collab-kit` runs on it: every component with made-up people, then
the connected components and the exported hooks against scripted
participants.

The registry Avatar gains an `xs` size and expresses its default size as
a variant so applet-scoped CSS cannot override it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Keep the collab capability type until its callers move off it

The people directory commit picked up an unrelated removal of
`CollabCapability` from the working tree, while the code that stops
using it is not committed yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Move collab availability into startup config

* Combine collab playground and optional dev identity setup

* Document the experimental collab architecture and dev workflow

* Remove the collab controls import cycle

* Limit collaboration to presence and add host user directories

Remove persistent shared-state synchronization, expose connected peers and separate presence reads/publications, and resolve profiles through authoritative workspace directories supplied by Batiok. Built-in indicators publish only while active; disabled applets keep inert APIs.

Validated with 1744 passing tests (4 platform skips), production build, lint, changed-file formatting, and typecheck excluding pre-existing invalid untracked sketch.tsx. Equivalent checks ran manually because the pre-commit hook formats the whole tree and would touch unrelated files.

* Add keyed list presence and expose workspace members

Show only connected people in the workspace header. Export the full workspace directory hook with status filtering, clarify applet/page scoping, and support one presence wrapper around keyed list or form children.

Verified 1754 tests, production build, two live browser sessions, filtered project typecheck, lint, and changed-file formatting. Ran checks manually instead of the whole-tree autoformat hook to preserve the existing invalid untracked sketch.tsx unchanged.

* Enable collaboration only through CLI flags

Forward --experimental-collab directly to child servers and initialize process configuration from parsed flags. Remove the environment toggle and service handling; keep init documentation installation separate from runtime opt-in.

Verified 1755 tests, lint, changed-file formatting, and project typecheck with the pre-existing invalid sketch.tsx excluded. Ran checks manually to preserve that untracked file from the whole-tree formatting hook.

* Handle missing collab bridges and isolate presence lifecycle

Return empty hooks and render nothing with one warning when an applet collab bridge is unavailable. Keep personal chat and view-builder creation from updating shared selection. Stop workspace presence, including pending connections, when the workspace is removed.

Addresses PR #147 review findings. Verified 1774 tests, production client build, lint, formatting, and project typecheck excluding the pre-existing invalid sketch.tsx. Ran checks manually to avoid the whole-tree hook modifying that unrelated untracked file.

* Consolidate collaboration engine and atomic host state

* Allow collaboration users without names

* Scope presence targets with explicit component and group IDs

* Include emails in collaboration hook examples

* Keep collaboration guide with workspace references

* Simplify collaborative applet authoring guide

* Keep applet messages in personal chats and verify review fixes

* Document collaboration component props and defaults

* Show Activity scopes in a dedicated collaboration demo

* Simplify experimental startup flags

* Remove redundant workspace layout provider key

* Simplify workspace collaboration boundary

* Inherit collab identity from Cloudflare Access (#148)

* Inherit collab identity from Cloudflare Access

A deployment behind Cloudflare Access (Zero Trust) can set
`cloudflareAccess` in config.json, or MOI_CLOUDFLARE_ACCESS_TEAM_DOMAIN and
MOI_CLOUDFLARE_ACCESS_AUD, so every viewer inherits their Access identity
instead of creating a dev profile.

The server verifies the Access application token (Cf-Access-Jwt-Assertion,
or the CF_Authorization cookie when a proxy drops the header): RS256
signature against the team's published keys, issuer, audience, and expiry.
The profile takes id from `sub` and email from `email`; tokens carry no
display name, so the name is the email's local part, and the color is a
stable pick from the persona palette hashed from the id.

GET /api/identity serves the verified profile and loads before the app
mounts. The browser uses it over a saved dev profile, and an outer host
still wins. Presence sockets verify the token on upgrade (401 without one)
and pin every join and identity update to the verified profile, so one
viewer cannot appear as another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Leave the name out of Cloudflare Access profiles

The base now allows users without names, with built-in labels falling
back to the email. Access tokens carry no display name, so the inherited
profile keeps only id, email, and the id-derived color instead of
deriving a name from the email. The /dev/collab notice labels the
profile with userDisplayName, which also fixes the typecheck against
the optional name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Condense Cloudflare Access tests

Fold the verifier rejections into one table, drop cases that repeat
other coverage (concurrent fetches, clock drift, profile helpers), and
merge the config, endpoint, socket pinning, and identity precedence
checks into fewer tests. Coverage stays on signature, issuer, audience,
expiry, key refresh throttling, header and cookie tokens, config
parsing, socket pinning, and source precedence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Refuse mismatched Access identities and expire cached keys

A presence socket verified by Cloudflare Access now joins only with the
verified id. A different id, such as an outer host's own identity, is
refused with identity_mismatch instead of being rewritten to the Access
user, so peers never see a different person than the browser shows.

Cached signing keys now expire after ten minutes, so a key Cloudflare
stops publishing stops verifying tokens even when no new key id arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Verify Cloudflare Access tokens with jose

Replace the hand-rolled JWT parsing, WebCrypto verification, and key
cache with jose's jwtVerify and createRemoteJWKSet, the library
Cloudflare's own verification examples use. Tokens must be RS256, name
the team domain as issuer and one of the configured AUD tags, and carry
an expiry, with 60 seconds of clock tolerance. jose caches the team's
keys for ten minutes and refetches for an unknown key id after a 30
second cooldown. Failed key fetches are logged; bad tokens are not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

* Widen the Access token time margins in tests

The not-yet-valid case set nbf 61 seconds past a timestamp taken at
module load, one second outside the verifier's 60 second tolerance, so
it failed whenever key generation in beforeAll took over a second, as
on a slow CI runner. Expired and not-yet-valid tokens now sit ten
minutes outside the tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ld6kvjMXg9eSvYsUJwo9NK

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Clarify collaboration names and tab state

* Clarify applet collaboration bridge types

* Remove obsolete collaboration error boundary

* Simplify avatar size guidance

* Allow host user profiles without colors

* Name proxy verified user consistently

* Remove obsolete collab layout cleanup

* Trim Batiok collab integration guide

* Use named user colors for collaboration

* Remove unused collaboration location title

* Resolve collab reference in server chat context

* Shorten browser tab session helper names

* Start unshipped collab protocol at version 1

* Unify applet URL resolution for pages and files

* Omit blank user profile names

* Trim applet and collaboration documentation

* Return undefined for unresolved collaboration users

* Load collaboration runtime from typed source

* Clarify applet runtime structure and worker messages

* Reuse public applet declarations as type contracts

* Use undefined for absent collaboration users

* Name cursor areas with id

* Use explicit status for collaboration connections

* Name collaboration applet scopes with appletId

* Simplify collaboration type names and share JsonValue

* Clarify applet presence naming and reuse applet IDs

* Use path-shaped applet IDs and rename AppletContainer

* Document the collaboration host bridge generically

* Trim collaboration docs and clarify applet guidance

* Round generated collaboration avatars

* Simplify collaboration structure and recover rejected joins

* Rename collaboration toolbar and fix scoped color token checks

* Simplify collaboration directory state and public hooks

* Initialize local collaboration users during startup

* Simplify collaboration user rendering and component types

* Check collaboration implementations against the public contract

* Compose user avatars and clarify collaboration docs and tests

* Clarify collaboration component names and playground examples

* Group dev UI component pages and simplify collab page naming

* Refine user component sizes and rename detail to description

* Simplify avatar badges and mask their background gap

* Reuse shared avatar groups and simplify count sizing

* Use transparent masks for avatar group overlaps

* Simplify collaboration user components and remove Activity

* Simplify collaboration badges and refine presence visuals

* Make collaboration playground interactive with two participants

* Unify collaboration presence components and simplify focus tracking

* Simplify presence frame geometry and add badge alignment

* Fix collaboration cursor entry and scroll positioning

* Add vertical alignment to presence avatars

* Clarify when to use collaboration presence components

* Simplify workspace collaboration users menu

* Polish workspace users menu

* Use fruit names for anonymous collaboration users

* Refine collaboration avatars and users menu ordering

* Restore avatar group overlap direction

* Add optional muted You label to collaboration users

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Anton Frehser <hey@tonyfresher.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants