Pilot decisions
The pilot is SP-initiated and defaults to denied access. A future enabled client selects exactly one active nonpersonal workspace. Each user needs current MEMBER status, an explicit client assignment, a confirmed email in an approved domain, fresh verified MFA no older than ten minutes, and identity-sharing approval on every login. Guests, invitations and email-domain matches alone confer no access. No workspace or test population has been selected by the protocol fixture. Share only a stable random pairwise NameID/id, confirmed email, firstName
and lastName. Persist subjects per client/Auth account; do not derive them from
email or recycle them. An email change requires fresh verification and operator
review of Claude linking behavior before restoring SSO. Names are presentation,
never authorization. Claude owns roles, seats and invite-only provisioning.
The pilot excludes OIDC, SCIM, groups, IdP-initiated login and single logout.
Tuturuuu logout does not imply termination of an existing Claude session.
These decisions apply to future outbound SSO Web/consent/management surfaces;
ordinary Tuturuuu sign-in and cross-app session policy retain their own rules.
Phase 1 protocol boundary
packages/sso provides strict Zod configuration/request/identity/assurance
contracts, bounded Redirect and POST AuthnRequest decoding, pinned request
signatures when required, signed response/assertion construction and public IdP
metadata without SLO. Clock, signer and ID generation are injected. Portable
entrypoints have no framework imports; the implementation targets Node crypto
and zlib for the future Next.js Node runtime on Vercel.
The selected libraries are xml-crypto 6.4.0 and @xmldom/xmldom 0.9.12.
RSA-SHA256 signatures, SHA256 digests and exclusive canonicalization are supplied
by the library. Request admission rejects unknown SPs, exact-URL mismatches,
stale requests, unsupported NameID/authentication contexts, passive interactions,
duplicate IDs/parameters, DTDs and oversized compressed/expanded XML. Unsigned
requests require an explicitly unsigned profile and do not authenticate the SP.
Assertions expire after two minutes and bind Audience, Recipient, Destination
and InResponseTo. The only supported NameID format is persistent; the connection’s
actual requirements must be confirmed before enabling it. For both request
bindings, NameIDPolicy or AllowCreate omission means false, including signed
requests. Only explicit true/1 permits the future caller to allocate a subject;
otherwise it must find an existing mapping or deny. These defaults apply to the
portable protocol and future outbound SSO adapters, with no unsigned-request
exception. See packages/sso/src/__tests__/security-review.test.ts for omission
and invalid-boolean regression coverage. REFEDS MFA context
requires a future adapter to prove primary authentication and current TOTP MFA;
the package cannot establish assurance from a supplied timestamp alone.
Phase 1 adds no routes, UI, migrations, live clients or hosted configuration.
Metadata generation is a library function, not a published metadata endpoint.
Do not expose the protocol functions as an issuance API without the later gates.
Phase 2 persistence boundary
Six additive migrations (20261022100000 through 20261022100005) introduce
manage_sso_clients and eight RLS-enabled private tables: sso_clients,
sso_saml_clients, sso_assignments, sso_subjects, sso_disclosure_grants,
sso_transactions, sso_signing_keys and sso_audit_events. All children carry
ws_id and composite tenant foreign keys. Creation always starts disabled;
client changes require an expected policy revision. The initial profile supports
only persistent NameID and one to eight exact HTTPS ACS destinations, without
credentials, fragments or custom ports. Configuration stores approved email
domains; authoritative email/domain enforcement belongs to phase 3.
Browser roles and service_role have no direct table privileges. Management is
through service-only sso_manage_client and sso_set_assignment RPCs. Both require
a current MEMBER in an active nonpersonal workspace with effective
manage_sso_clients permission (including the existing creator/admin semantics).
The server must resolve the authenticated actor before calling them; an actor ID
is a trusted server input, never a browser authorization claim.
Service-only sso_create_transaction reserves a request hash uniquely per client,
stores InResponseTo and an exact configured ACS, and caps expiry at five minutes.
Opaque handles, browser/session bindings and RelayState are hashes; raw RelayState
and assertions are absent. sso_record_consent records one immutable per-login
approval or denial for the fixed identity attribute list, binding actor, policy,
browser and session. Denial burns the transaction. sso_consume_transaction takes
row locks, rejects expired/used transactions, rechecks enabled client, policy,
current MEMBER and active assignment, and requires the matching approval and
bindings. The validated request’s allow_create is persisted with default false;
allocation requires explicit true, while false can reuse an existing mapping.
Consumption, random 256-bit pairwise subject allocation and audit append
commit together. Concurrent consumers yield one winner and a replay error.
authenticated users can call sso_read_own_disclosures(ws_id) to read up to 100
of their own receipts. Its actor comes exclusively from auth.uid(). Other users
and anonymous callers cannot read those receipts. Disclosure receipts and audit
rows reject updates and deletes; audit appends expose only bounded event codes
and correlation IDs. sso_append_audit is service-only and requires a consumed,
approved transaction for issuance events. A signed event records the issuer’s
observation; it does not prove Claude accepted the response.
Signing-key rows contain only a public certificate, kid, status, revision and
validity/rotation timestamps. A partial unique index permits at most one active
key per client. Certificate syntax constraints reject private-key PEM; the future
signer must validate the actual X.509 certificate and key match. No private key,
secret reference, key import/rotation RPC or signer route is added by this phase.
Subjects reference auth.users, so actual Auth deletion clears their user link
while reserving the random value. Auth deletion also clears client/assignment
creator attribution and deletes assignments and actor-bound transactions, even
though the public user row remains. Membership deletion cascades assignments;
MEMBER-to-GUEST demotion deletes them too. Rejoining or later promotion requires
an explicit new assignment. This rule applies to every phase 2 SSO client.
Workspace hard deletion cascades clients, protocol profiles, assignments, subjects,
transactions and public signing-key metadata, including disabled clients. Immutable
disclosure receipts and audit events retain denormalized workspace/client/actor/
transaction UUIDs without parent foreign keys. These historical receipts cannot
authorize a login without a live transaction and current membership/assignment.
Cleanup never disables or bypasses their append-only triggers. All eight tables
force RLS; approved definer RPCs retain their PostgreSQL BYPASSRLS owner, while a
non-BYPASSRLS table owner is subject to the policies.
Receipt/audit actors are UUID-only correlation values without deletion-blocking
user foreign keys; retention and pseudonymization require the phase 5 operating
policy. There is no automatic retention job in this phase.
Database verification
From the repository root, run:supabase start and supabase db reset, runs
sso-persistence.sql with strict pgTAP parsing, and drives a real two-connection
consume race. It generates packages/types/src/supabase.ts from that same applied
public,private,storage schema, then stops only the owned database without backup.
Synthetic race rows are committed only inside the disposable database and vanish
with its teardown; immutable audit rows are never bypassed for fixture cleanup.
Each race uses fresh UUIDs so repeat runs preserve retained user/workspace history.
The pgTAP packet covers membership removal/rejoin, demotion/promotion, real Auth
deletion (including retained public users), creator and workspace deletion,
byte-identical retained history, and a non-BYPASSRLS table owner. The separate race
requires one winner, one replay rejection, one subject and one consume audit.
It also races membership deletion and assignment deactivation against consume in
both orders. The holder observes its own PID blocking the competitor before it
commits: revocation first must deny consume without a subject or consume audit;
consume first must commit before revocation can commit. Both orders deny a later
consume. Assignment deactivation uses direct SQL so the management RPC’s client
lock cannot mask missing authorization-row locks. Keep lock-removal negative
controls confined to a disposable database; never alter production migrations
to make the harness pass.
When the committed base type snapshot already lags its applied schema, generate
both the clean base and candidate using the same CLI and formatter. Apply their
generated delta to the committed base with a three-way merge, failing on conflicts;
exclude unrelated baseline drift rather than manually editing generated objects.
The SSO-only committed type delta intentionally excludes existing employee-profile
objects absent from the base snapshot. The workflow artifact still contains the
complete applied schema. Check packages/users-core as well as the types package
after generation because new FK relationships affect nested select inference.
The runner prints a schema snapshot digest and recovery directory for a failed
cleanup. Use the existing isolated lifecycle cleanup procedure for that directory.
The SSO persistence database contract CI workflow repeats this on the exact PR
head. No application stack or build is needed for this check.
These primitives do not establish Auth account status, confirmed email, MFA,
request XML validation, key selection or an issuance permit. Phase 3 must provide
those checks, recheck current key/policy state after signing, and burn the original
transaction on an unknown signing outcome. Never interpret a consumed database
row as permission to retry or emit an assertion without those service gates.
Verification and remaining work
packages/sso/src/__tests__ drives generated signatures through Node-SAML’s SP
consumer with disposable in-memory keys. Negative coverage includes wrapping,
tampered claims, unsigned assertions, wrong audience/ACS, expiration, replayed
InResponseTo, unknown SPs and deflate bombs. Node-SAML uses the same underlying
signature library; independent SAML parsing is not independent crypto validation.
Parser and XML-signature library failures expose fixed protocol error messages
without request-derived diagnostics. Wrapping regressions retain an intact signed
assertion beside a distinct unsigned attacker assertion, verify both original
assertion and renewed outer signatures, and assert semantic rejection.
The harness adds exact Destination/Recipient checks because Node-SAML 5.1.0 does
not enforce them for login responses. See packages/sso/README.md for dependency
advisories, limits, reproduction commands and the samlify evaluation.
Phase 2 supplies the private database primitives described above. Phase 3 supplies
authoritative Auth/MFA/eligibility reads,
disclosure enforcement, key/policy revision fences, durable audit and disabled
Web routes. Phase 4 supplies bilingual management and per-login consent UI.
Phase 5 supplies rotation, compromise/disable procedures, an explicitly selected
test workspace and connection, exact-commit CI/runtime acceptance and real Claude
SSO tests. Runtime profile verification and actual Claude login remain unrun.
Run focused tests through ttr resources run -- bun run --cwd packages/sso test
and the package’s type-check script. Require applicable exact-commit CI before
integration; no local release builds or production configuration changes are
part of this phase.