feat(orgs): consent tenant-pinning + external-mode guardrails (Phases 6-7) #8
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/company-access-modes-phase6"
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
Phases 6 and 7 of "company access modes", stacked on Phase 3 (PR #7) — this PR targets that branch, so its diff is Phases 6-7 only.
Phase 6: consent tenant pinning
When a relying party's
/oauth2/authrequest carriesorganization_id=X, the id_tokenorgandgroupsclaims must reflect X and must never assert a different tenant. Today consent falls back tomemberships.first()when no active-org cookie matches, so an RP that requested org X could receive org Y's claim for a user in both — a wrong-tenant assertion.resolve_claim_active_orgnow pins the claim:org/groupsclaim (suppressed; never a different org).organization_idon the request → unchanged (active-org cookie, else first membership).The pin is applied at both
finalize_consentcall sites (the auto-grant/skip path and the interactive Allow path), so askip_consentclient cannot bypass it.Scope note: the plan's RP-initiated join interstitial (auto-joining a brand-new user into an org mid-consent) is a Model-2 / B2B customer-tenant pattern and is deferred to that model. In Model 1 the self-serve join path is the landing page (Phase 3). This PR fixes the claim correctness; it does not auto-join a stranger.
Phase 7: external-mode guardrails
/o/{slug}and/registration: the existing per-IPdual_windowplus a new global (all-callers-share-one-bucket) pair, so distributed signup and X-Forwarded-For spoofing (which per-IP alone can't catch) are bounded. All limits are config knobs with sane code-side defaults. A per-org dimension is not built (a documented limitation):/registration's target org lives inside the opaque Kratos flow, not the URL, so a per-org key extractor isn't cheaply buildable there.docs/commercial/organizations.md), operators (docs/operator-guide.md— the new config knobs, thetrust_forwarded_forprerequisite, and the deliberate no-verification-gate-on-join design), and contributors (docs/dev/organizations-internals.md).CAPTCHA: deferred by design, not built
Forseti's
/registrationis GET-only; the browser submits registration directly to Kratos's own public endpoint, which Forseti never sees. A real Turnstile gate would need a blocking Kratosbeforehook, a new Forseti verify webhook, and a client-side widget — a multi-system integration disproportionate to this phase. A client-side-only widget would be a placebo with no server enforcement. The gap and the mechanism a real fix needs are documented instead. Rate limiting plus the existing unverified-account reaper bound the abuse surface in the meantime.Verification
cargo clippy --all-targets -- -D warnings: clean. Full unit suite: 566 passed, 0 failed. i18n parity: pass.Known follow-ups
/registration?org=&flow=redirect instead of relying on Kratos's opaquereturn_to.Pull request closed