Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions changelog.d/2-features/multi-ingress-cross-IdP-SSO
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
When a team uses multiple SAML IdPs (one per ingress domain) in a multi-ingress
setup, users can now authenticate via any of the team's IdPs even if their
account was originally provisioned under a different one. Spar resolves the
correct account by email-based NameID lookup across all team IdPs and migrates
the user's SSO identity to the authenticating IdP transparently.

**Important:** Email addresses (`NameID`s) must be unique across configured
IdPs! Otherwise, users may be logged in into wrong accounts!

Please refer to the documentation for further information.
57 changes: 56 additions & 1 deletion docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,8 @@ Given an email address, the SSO code is looked up by these criteria:
This is the case for:
- Teams with exactly one configured IdP
- There is an IdP for the given multi-ingress domain
- The user was created via SCIM
- The user is a SSO user. So it was created via SCIM with SSO enabled (any IdP
configured in SCIM token) OR created via SSO (no SCIM involved).

The last condition ensures that team admins cannot get into locked-out
situations due to misconfigured IdPs.
Expand Down Expand Up @@ -1500,6 +1501,60 @@ error. Though, IdPs can be reconfigured as long as this invariant holds.

Putting it differently: We require an unambiguous mapping `(team, domain) -> IdP`.

#### Multi-ingress cross-IdP SSO (fallback)

Terms used below:

- _Authenticating IdP_ — the external identity provider that issued the SAML
assertion, identified by the `Issuer` URI inside it.
- _IdP configuration_ — backend's IdP representation registered via
`/identity-providers`, storing the issuer URI, the associated multi-ingress
domain, and the team.

In the normal SSO flow spar looks up the authenticating user by their `(issuer,
NameID)` pair — matching the assertion's issuer against the IdP configuration
the user was provisioned under.

In a multi-ingress setup each domain has its own IdP configuration with its own
issuer URI. A user provisioned under domain _A_ has their SSO identity tied to
issuer _A_'s URI. When that user later authenticates via domain _B_, the IdP
authentication response's assertion carries issuer _B_'s URI, so the primary
`(issuer, NameID)` lookup finds nothing. Using a shared static issuer across
all domains is not an option because each external identity provider has its
own issuer URI that spar cannot control.
Comment on lines +1522 to +1524
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this sentence. How about:

Suggested change
`(issuer, NameID)` lookup finds nothing. Using a shared static issuer across
all domains is not an option because each external identity provider has its
own issuer URI that spar cannot control.
`(issuer, NameID)` lookup finds nothing. Two IdPs can't have the same Issuer ID because those must be globally unique. Sharing the IdP for all multi-ingress domains is not an option because that would reveal the connection between the domains to the attacker.

If yours is more correct, I want to understand how.


When this primary lookup finds no user, spar therefore attempts a cross-IdP
migration when multi-ingress is configured:

1. **NameID must be an email address.** Username-based `NameID`s are rejected
to avoid ambiguity across authenticating IdPs.
Comment on lines +1529 to +1530
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] admins need to manually guarantee that email addresses are unique, so why can't they do the same thing here?

2. **The matching IdP configuration is resolved.** Spar looks for an IdP
configuration in the team whose issuer URI and configured domain both match the
assertion's issuer and the incoming `Z-Host` header (exact match). If no exact
match is found and the team has exactly one IdP configuration, that one is used
unconditionally (no issuer or domain check). If neither condition is met, the
Comment on lines +1533 to +1535
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pick a non-matching IdP, and not just reject right away?

login is rejected.
3. **Team-wide user search.** Spar searches all of the team's IdP
configurations for a user whose email NameID matches the subject. This can be
understood as trying to login with all team IdP configurations. This step also
_finds_ the user, before we found them we don't know their IdP. (So, we can't
simply use their IdP first.)
4. **Migrate or provision:**
- _Exactly one match found:_ The user's SSO identity is updated to point to
the IdP configuration for the authenticating IdP's issuer, so subsequent
logins hit the primary lookup directly. This saves the complexity of the IdP
configuration lookup and keeps the backend's representations of the user's
SSO data sound.
- _No match found:_ A new user account is auto-provisioned under the
authenticating IdP's configuration.
- _No matching IdP configuration can be resolved:_ Login is rejected.

##### Security considerations

It must be ensured that email `NameID`s are unique across IdPs by IdP
administrators. Otherwise, users are falsely logged in into other user's
accounts!

### Webapp

The webapp runs its own web server (a NodeJS server) to serve static files and the webapp config (based on environment variables).
Expand Down
1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ library
Test.Shape
Test.Spar
Test.Spar.GetByEmail
Test.Spar.MultiIngressCrossIdpSso
Test.Spar.MultiIngressIdp
Test.Spar.MultiIngressSSO
Test.Spar.STM
Expand Down
Loading
Loading