Mirrored from GitHub github.com/roostorg/coop

auth: fix SAML cross-org authentication bypass (#783) master

* Fix SAML cross-org authentication bypass The SAML verify callbacks looked up the user by email alone (kyselyUserFindByEmail), ignoring the org named in the callback path (/saml/login/:orgId). Because the same email can exist across tenants, an assertion signed by one org's IdP could resolve a user belonging to a different org and create a session as that cross-tenant user. Bind the lookup to the path org via a new org-scoped kyselyUserFindByEmailAndOrg, used by a shared resolveSamlUser helper for both the signon and logout verify callbacks. A user from another org is no longer found, so login fails. Addresses GHSA-2v93-383c-9fw2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Reject missing/invalid SAML email claim before lookup resolveSamlUser coerced the email claim with String(profile?.email), turning a missing claim into the literal "undefined" (and an array claim into a comma-joined string) and using it as a lookup key. Validate that the claim is a non-empty string and reject the authentication attempt before querying otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Refactor resolveSamlUser to receive KyselyPg directly Match the codebase DI convention: consumers receive the Bottle-provided KyselyPg and call the free persistence functions directly (as UserApi, RoleApi, etc. do), rather than injecting a bespoke findUser closure. resolveSamlUser now takes db: UsersDb and calls kyselyUserFindByEmailAndOrg itself; api.ts passes KyselyPg. Its tests move to the real-DB testWithFixture pattern used for the persistence layer, exercising the org-scoped lookup against Postgres. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Extract getOrgIdFromPath helper and log SAML login DB failures - Add a shared getOrgIdFromPath(req) helper and use it in both getSamlOptions and resolveSamlUser to DRY the orgId path-param check. - Thread the Tracer into resolveSamlUser and narrow the catch to the DB lookup so a genuine outage during login is logged via logActiveSpanFailedIfAny (observable) and surfaced as an internal error, instead of swallowing the done() dispatch in a broad catch. - Share one verify callback for the SAML signon and logout slots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>