@@ -1331,7 +1331,8 @@ Given an email address, the SSO code is looked up by these criteria:
13311331- The mapping must be unambiguous (there must be exactly one matching IdP).
13321332 In multi-ingress mode, IdPs are always bound to one domain; the request domain
13331333 must match the IdP's configured domain.
1334- - The user was created via SCIM
1334+ - The user is a SSO user. So it was created via SCIM with SSO enabled (any IdP
1335+ configured in SCIM token) OR created via SSO (no SCIM involved).
13351336
13361337The last condition ensures that team admins cannot get into locked-out
13371338situations due to misconfigured IdPs.
@@ -1614,6 +1615,60 @@ Putting it differently: We require an unambiguous mapping `(team, domain) -> IdP
16141615
16151616For multi-ingress setups, the [`idpCertFingerprintAllowlist`](#idp-certificate-fingerprint-allowlist)
16161617must be configured to restrict which X.509 certificates can be used in IdP metadata.
1618+ This restriction was introduced to mitigate risks of
1619+ [Multi-ingress cross-IdP SSO (fallback)](#multi-ingress-cross-idp-sso-fallback).
1620+
1621+ # ### Multi-ingress cross-IdP SSO (fallback)
1622+
1623+ Terms used below :
1624+
1625+ - _Authenticating IdP_ — the external identity provider that issued the SAML
1626+ assertion, identified by the `Issuer` URI inside it.
1627+ - _IdP configuration_ — backend's IdP representation registered via
1628+ ` /identity-providers` , storing the issuer URI, the associated multi-ingress
1629+ domain, and the team.
1630+
1631+ In the normal SSO flow spar looks up the authenticating user by their `(issuer,
1632+ NameID)` pair — matching the assertion's issuer against the IdP configuration
1633+ the user was provisioned under.
1634+
1635+ In a multi-ingress setup each domain has its own IdP configuration with its own
1636+ issuer URI. A user provisioned under domain _A_ has their SSO identity tied to
1637+ issuer _A_'s URI. When that user later authenticates via domain _B_, the IdP
1638+ authentication response's assertion carries issuer _B_'s URI, so the primary
1639+ ` (issuer, NameID)` lookup finds nothing. Two IdPs can't have the same Issuer
1640+ ID because those must be globally unique, and each external identity provider
1641+ controls its own issuer URI (spar can't override it).
1642+
1643+ When this primary lookup finds no user, spar therefore attempts a cross-IdP
1644+ migration when multi-ingress is configured :
1645+
1646+ 1. **NameID must be an email address.** Username-based `NameID`s are rejected
1647+ to avoid ambiguity across authenticating IdPs.
1648+ 2. **The matching IdP configuration is resolved.** Spar looks for an IdP
1649+ configuration in the team whose issuer URI and configured domain both match
1650+ the assertion's issuer and the incoming `Z-Host` header (exact match).
1651+ If this condition is unmet, the login is rejected.
1652+ 3. **Team-wide user search.** The authenticating IdP is now known (step 2), but
1653+ the user may still be registered under a *different* team IdP from an
1654+ earlier login. Spar therefore searches every IdP configuration in the team,
1655+ pairing each one's issuer with the assertion's email NameID, and tries the
1656+ primary `(issuer, NameID)` lookup for each pairing until one matches.
1657+ 4. **Migrate or provision:**
1658+ - _Exactly one match found:_ The user's SSO identity is updated to point to
1659+ the IdP configuration for the authenticating IdP's issuer, so subsequent
1660+ logins hit the primary lookup directly. This saves the complexity of the IdP
1661+ configuration lookup and keeps the backend's representations of the user's
1662+ SSO data sound.
1663+ - _No match found:_ A new user account is auto-provisioned under the
1664+ authenticating IdP's configuration.
1665+ - _No matching IdP configuration can be resolved:_ Login is rejected.
1666+
1667+ # #### Security considerations
1668+
1669+ It must be ensured that email `NameID`s are unique across IdPs by IdP
1670+ administrators. Otherwise, users are falsely logged in into other user's
1671+ accounts!
16171672
16181673# ## Webapp
16191674
0 commit comments