multi-ingress: cross-IdP SSO#5212
Conversation
70c63ab to
63c5b7d
Compare
c70220c to
e7f3bf2
Compare
e7f3bf2 to
d27326c
Compare
There was a problem hiding this comment.
Pull request overview
Adds support for multi-ingress “cross-IdP” SSO logins in Spar by falling back to a team-wide lookup (by email NameID) when the primary (issuer, NameID) lookup fails, and updates /sso/get-by-email discovery to work for any SSO user (not only SCIM-managed).
Changes:
- Extend Spar SAML finalize-login handling to support cross-IdP migration in multi-ingress mode (and add a dedicated config error for non-email NameIDs).
- Relax
/sso/get-by-emaillookup condition from “SCIM+SSO” to “SSO user”. - Add integration and unit tests plus documentation/changelog updates covering the new behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| services/spar/test-integration/Test/Spar/AppSpec.hs | Updates integration test helper to call updated verdictHandler signature with SAML config. |
| services/spar/src/Spar/Error.hs | Adds and renders a new error for invalid multi-ingress cross-IdP configuration (non-email NameID). |
| services/spar/src/Spar/App.hs | Implements multi-ingress cross-IdP fallback flow + structured logging changes. |
| services/spar/src/Spar/API.hs | Plumbs SAML config/host info into finalize-login handling and updates logger constraints. |
| libs/wire-subsystems/test/unit/Wire/IdPSubsystem/InterpreterSpec.hs | Adjusts unit test expectation to “non SSO user” behavior for get-by-email. |
| libs/wire-subsystems/src/Wire/IdPSubsystem/Interpreter.hs | Changes /sso/get-by-email eligibility check from SCIM+SSO to SSO. |
| integration/test/Test/Spar/MultiIngressCrossIdpSso.hs | Adds new integration test suite for cross-IdP migration and related error cases. |
| integration/integration.cabal | Registers new integration test module. |
| docs/src/developer/reference/config-options.md | Documents cross-IdP fallback algorithm and updates get-by-email criteria. |
| changelog.d/2-features/multi-ingress-cross-IdP-SSO | Adds changelog entry describing cross-IdP SSO behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -- This used to check if the user is SCIM AND SSO! The RFC ("2025-05-12 | ||
| -- RFC: Default SSO flow for team by host domain") is not really | ||
| -- unambiguous about this. The customer currently provisions non-SCIM. | ||
| isSsoUser :: User -> Bool | ||
| isSsoUser = isJust . userSSOId |
There was a problem hiding this comment.
This might be subject to change in case the customer decides to go for SCIM. Then, we could make this condition stricter (requiring SSO AND SCIM).
fisx
left a comment
There was a problem hiding this comment.
I read the docs and have some questions, more feedback coming up!
| 1. **NameID must be an email address.** Username-based `NameID`s are rejected | ||
| to avoid ambiguity across authenticating IdPs. |
There was a problem hiding this comment.
[nit] admins need to manually guarantee that email addresses are unique, so why can't they do the same thing here?
There was a problem hiding this comment.
@emil-wire and I were uncertain about this as well. I made the hand-wavy decision, because in my experience with other web services, my email address is much less likely to conflict than my desired username when I register somewhere.
However, the customer is not much into emails as fetching those may reveal relationships as well AND @julialongtin promised that all data will be located in one database (used by all IdPs)
So, this could actually be subject to change. 🤔
There was a problem hiding this comment.
I think the best approach is to allow nicks / usernames, but put big red warnings stickers in all the right places and emphasize that emails are best here, and nicks with mandatory, instance-specific pre- or suffixes are second-best.
If you decide against my intuition that's also fine, if anything supporting only emails would make it safer.
a149340 to
4cce453
Compare
82b25a8 to
f9fea47
Compare
f9fea47 to
c134d5d
Compare
4113c24 to
9a88a59
Compare
9a88a59 to
cc9b20e
Compare
e54062a to
9e877f6
Compare
fisx
left a comment
There was a problem hiding this comment.
I skimmed through the integration tests and thoroughly reviewed the rest, looks intact! I've hidden a few possibly valuable change requests in a large number of nit-picks to make processing the review more fun. :-)
I'm pre-approving because none of the change requests affect correctness and security IMO.
| 1. **NameID must be an email address.** Username-based `NameID`s are rejected | ||
| to avoid ambiguity across authenticating IdPs. |
There was a problem hiding this comment.
I think the best approach is to allow nicks / usernames, but put big red warnings stickers in all the right places and emphasize that emails are best here, and nicks with mandatory, instance-specific pre- or suffixes are second-best.
If you decide against my intuition that's also fine, if anything supporting only emails would make it safer.
| It must be ensured that email `NameID`s are unique across IdPs by IdP | ||
| administrators. Otherwise, users may be logged into other users' accounts! |
There was a problem hiding this comment.
It's probably worth adding this warning to the UI in team management, at the point where multi-ingress is enabled. Has this happened? Is there a ticket for this?
728c3e7 to
7cb0def
Compare
Allow users to login with all team IdPs. This is required because there's usually one IdP per ingress in a team and users should be able to login on all ingresses.
700aa26 to
028e41e
Compare
| pairing each one's issuer with the assertion's email NameID, and tries the | ||
| primary `(issuer, NameID)` lookup for each pairing until one matches. | ||
| 4. **Migrate or provision:** | ||
| - _Exactly one match found:_ The user's SSO identity is updated to point to |
There was a problem hiding this comment.
Hm, please correct me if I'm wrong (my Haskell isn't great) but doesn't https://github.com/wireapp/wire-server/blob/sventennie/multi-ingress-sso_cross_idp/services/spar/src/Spar/App.hs#L528
asum $ map tryIdP idps return the first matching (issuer, email NameID) across team IdPs? Meaning if there are more than one match, the "Exactly one" condition isn't satisfied?
Failure scenario:
- Team has IdP A and IdP B.
- Victim exists under IdP A with email x@example.
- A different person or malicious IdP-side actor authenticates via IdP B with NameID = x@example.
- If no primary (B, x@example) user exists, fallback searches all team IdPs and can select the victim’s IdP A mapping.
- Outcome: attacker logs into victim’s Wire account and the SSO identity is migrated to IdP B.
This is exacerbated by the fact that we assume (but never check in code): "Different SAML IdPs must have a same user set” as a prerequisite.
There was a problem hiding this comment.
asum $ map tryIdP idps return the first matching (issuer, email NameID) across team IdPs? Meaning if there are more than one match, the "Exactly one" condition isn't satisfied?
Yes, if you take this precisely for this expression: Yes, there could be e.g. 2 matching IdPs and the last one found would be ignored. Probably, this item is a bit misleading: SSO users are always bound to one IdP. The systems doesn't allow for more than one, though.
I will update the documentation accordingly.
Failure scenario:
Team has IdP A and IdP B. Victim exists under IdP A with email x@example. A different person or malicious IdP-side actor authenticates via IdP B with NameID = x@example. If no primary (B, x@example) user exists, fallback searches all team IdPs and can select the victim’s IdP A mapping. Outcome: attacker logs into victim’s Wire account and the SSO identity is migrated to IdP B.This is exacerbated by the fact that we assume (but never check in code): "Different SAML IdPs must have a same user set” as a prerequisite.
Yes, that's exactly the scenario we're dealing with here (if you change the wording a bit, that's exactly the usecase - just s/(attacker|victim)/user/g 😉 ).
Unfortunately, we cannot ensure on our end that two IdPs contain the same user data.
Why do we need multiple IdP configurations (the representation of an IdP at Wire backend) then? A common IdP endpoint would leak information about the relationship of ingress-domains.
The countermeasures for this are:
- Logging on IdP configuration change (https://wearezeta.atlassian.net/browse/WPB-22124)
- Emails on IdP configuration change (https://wearezeta.atlassian.net/browse/WPB-22124)
- IdP fingerprint allowlist in spar's application config (https://wearezeta.atlassian.net/browse/WPB-26150 , with Jonas' analyses https://wearezeta.atlassian.net/browse/WPB-25378?focusedCommentId=163359)
|
Not something this PR introduced, but its close enough to it that I'd rather raise it here than lose track of it and forget about it. I don't think the rate limit on I rendered the chart to check ( So the endpoint ends up on 50r/s instead of the intended 5r/m. The initiate-login and finalize-login are already above raisin it here because the two things interact: this PR widens which users the endpoint answers for (SCIM+SSO -> any SSO user), and config sets |
I've covered this usecase in the documentation. To avoid duplication, please look at the docs in this PR.
Ticket: https://wearezeta.atlassian.net/browse/WPB-25161
Security notes:
/sso/get-by-emailfrom SCIM SSO to SSO userFurther reading:
Checklist
changelog.d