feat(orgs): external self-serve join (Phase 3) #7
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/company-access-modes-phase3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
Phase 3 of "company access modes": the external self-serve join path. A public landing page (
/o/{slug}) for an external org sends a visitor into registration, they land on an explicit/join/confirmpage, confirm, and become a member of THAT org instead of the hardcoded Default org. This closes the gap where/o/{slug}was cosmetic and every registrant landed in Default.Stacked on the Phases 1-2 branch (PR #6) — this PR targets that branch, so the diff is Phase 3 only.
Mechanism (validated by live-stack keystone spikes)
Before implementing, three spikes were run against the running Kratos/Hydra playground. They confirmed the mechanism and, importantly, made it simpler than first drafted:
hook: session), so a fresh registrant lands authenticated on areturn_towe control.allowed_return_urlsis origin-prefix, so/join/confirmneeds no Kratos config change./join/confirm) is enough — no identity metadata needed.Included
add_member_race_safe(db, identity, org, role): a race-safe single-INSERT joiner (unique constraint as the guard). Owner-promotion stays Default-only; the existing Default auto-join transaction is untouched.resolve_signup_org(slug): fail-closed — returns an org only if it exists, isexternal, and haspublic_login_enabled=1, re-read at call time./o/{slug}binds the registration CTA toreturn_to=/join/confirm?org=<slug>for external+enabled orgs; internal orgs keep the plain/registrationlink./join/confirmis not force-joined to Default./join/confirmGET+POST: an explicit, CSRF-protected confirmation modelled on the invite-accept flow. GET 404s unknown/internal/disabled slugs (anti-enumeration), renders a register CTA when anonymous, redirects already-members, and shows the confirm form otherwise. POST re-resolves the org (TOCTOU-safe), joins asmember, is idempotent, and audits the join as self-serve.Explicitly not included
No
metadata_adminwrites, noverification.completedwebhook, no email-verified gate (removed by the keystone revision). RP-initiatedorganization_id/consent-interstitial ordering is Phase 6. External guardrail hardening (rate limiting, CAPTCHA, audit of mode changes) is Phase 7.Verification
cargo clippy --all-targets -- -D warnings: clean. Full unit suite: 561 passed, 0 failed. i18n parity harness: pass.return_toround-trip) were exercised live during the keystone spikes.h-org-external-self-serve-join.spec.ts), which needs the licensed playground stack to run. It was not run here, and the flow was not exercised against a from-this-branch running portal (that would mean restarting the running instance).Known follow-ups
POST /join/confirmproduces one membership row but could emit two audit entries (narrow race, audit-noise only).Pull request closed