┌─ 404human ─────────┐
  │  $ whoami          │
  └────────────────────┘
404human
Rust runtimes, background jobs, AI infrastructure

Colophon build log P7 · own-auth

· 4 min read · release

Checkpoint P7b: our own auth platform — magic links, no required provider, audited sign-ins

Colophon's audience auth is now self-contained in the Supabase/Convex sense: passwordless magic links beside email+password, no Keycloak or external provider required anywhere in the product, and every visitor sign-in event lands in the same audit log staff already read.

at a glance
Goal
Make the platform's built-in authentication complete enough to run a real installation alone: passwordless sign-in for visitors, no hard dependency on Keycloak or any external identity provider, and staff-visible logs for everything the auth system does.
Built
Single-use hashed magic links (signup-by-link into a project's open role, anti-enumeration 202s), a public /auth/providers endpoint so the Studio never offers a sign-in button that can only fail, CLI bootstrap for built-in accounts, every visitor auth event audited under a shared system actor, a Studio Audience page for staff (users, roles, disable/enable, sign-in activity), a usability pass on navigation and scale, and real delivery: a mail-provider seam with a Resend adapter behind three env vars — dev_mail only for development.
Result
6/6 database tests — single-use, supersession, expiry and project-scope proven at Postgres RLS, not around it · 5/5 black-box HTTP tests — dev-mail on/off, request→consume→gated-read, no-open-role refusal · extended k6 smoke: 1,679 requests, 0 failures, magic_consume p99 36ms, member reads p99 ~10ms · real-browser e2e: the Audience page driven against a live server — manage flows, the permission boundary hiding it entirely, zero requests to refused endpoints · visitor signup/signin/signout/magic-link events all asserted present in the project's /audit feed · Resend e2e against a local receiver: the whole provider call asserted — bearer, sender, recipient, subject, link in both bodies — the emailed token signs in once, and a scripted provider-500 still answers sent
Code
akira-shirase-oss/colophon-cms · main @ 3bf5938

The previous checkpoint built the audience itself; this one makes the platform’s auth its own product rather than a Keycloak integration. Three seams closed:

Passwordless, built in

POST /audience/v1/{project}/magic-link records a single-use, SHA-256-hashed, 15-minute link and answers the same HTTP 202 whether or not the email has an account — enumeration is a response-level property, so both cases look identical. POST …/magic-link/{token} spends it once: an existing account signs in, an unknown email becomes a site user in the project’s open self-assign role with an unusable password, and any other outcome — used, expired, superseded, wrong project — is the same magic_link_invalid. Requesting a second link retires the first, so an old link can never outlive the newest request. return_to must be an absolute http(s) URL, so the link always lands on the site that asked.

There is no mail sender yet — that is the honest remaining gap. COLOPHON_DEV_MAIL stands in on loopback only: it hands the link back in the response for development and demos, and the server refuses to boot with it set on a public listener, because on a public bind it is an account-takeover switch, not a feature.

No required provider

An installation now runs entirely on built-in accounts. GET /auth/providers publicly answers whether an external provider is configured, so the Studio’s sign-in card hides “Sign in with your organisation” instead of offering a button that can only 503. colophon admin bootstrap --issuer password --subject [email protected] makes a password account the first administrator — before this, the CLI demanded an issuer URL even on an install with no provider. OIDC remains an adapter a deployment may add; it is no longer an assumption.

Auth the staff can see

Every visitor auth event is in the project’s append-only audit feed — audience.user_signed_up, .user_signed_in, .user_signed_out, .magic_link_sent, .magic_link_spent — readable through the same /audit endpoint staff already use. The actor is a shared urn:colophon:system identity (the pattern the worker’s budget events already used), because audit_events.actor_id references staff identities and a site user is not one. The write runs after the auth commits, best-effort by contract: auth correctness never depends on the log, and the log never becomes a second auth path.

And the log has a home: the Studio’s new Audience page sits next to Members — a different credential kind, a different page. It lists the project’s site visitors (role, last sign-in, disabled state), lets a role holder change roles or disable an account, shows the audience roles, and surfaces a “Sign-in activity” panel that filters the same /audit feed to audience.*. Members are staff; the Audience are your readers — the Studio now shows both.

A usability pass followed (D-144): the sidebar’s catch-all “Project” group became People (Members, Audience — the staff/visitor split is taught by position now, not by a subtitle) and Structure (Content types, Migrations, Settings). “All projects” caps at 24 and asks you to search, because the dev database’s hundreds of test projects proved an unbounded grid is where projects go to hide. Zero-count badges went quiet, “Last published” admits “Never”, and the browser suite asserts the cap by finding its project through search — the intended behaviour, not a workaround.

Then the last gap closed (D-145): magic links now reach a real inbox. Services.mail holds an optional Arc<dyn Mail> — a provider seam rather than a hardcoded sender — and COLOPHON_MAIL_PROVIDER=resend + COLOPHON_MAIL_FROM + COLOPHON_RESEND_API_KEY builds the first adapter: one POST to Resend’s emails endpoint under a bearer key and a ten-second deadline, with COLOPHON_RESEND_ENDPOINT to move the address for relays or tests. The send is best-effort after the link row commits: a refused send warns in the server log but can never change the 202 (the response stays enumeration-proof) or undo the recorded link — asking again delivers a fresh one. Config is all-or-none like COLOPHON_S3_*: a partial setup, an unknown provider, or a stray endpoint fails at boot, not silently. The end-to-end proof runs the real server against a local receiver and asserts the whole provider call — bearer, sender, recipient, subject, the link in both bodies — then spends the emailed token into a real session, and a scripted provider-500 still answers sent. dev_mail still wins when both are set, so development needs no provider at all. SMTP and operator-managed providers land on the same seam when needed.

Still open: SMTP as a second provider, social/OAuth sign-in for visitors, and billing/subscription entitlements (deliberately deferred — roles are labels, not plans).


← previous · audience-auth
Built-in audience auth
next · 24h soak →
Soak failures, three fixes, honest gates

← All writing