Issue Summary
When Polis builds the SAML LogoutRequest, it uses the global opts.samlAudience as the <saml:Issuer> and never reads the connection's samlAudienceOverride. The login path does use the override (connection.samlAudienceOverride ?? samlAudience), so any connection with a samlAudienceOverride sends one Issuer at login and a different one at logout.
Whether that mismatch actually breaks logout comes down to how strictly the IdP checks the session participant. Microsoft Entra / Azure AD checks it strictly: it recorded the participant under the override Issuer at login, then gets a logout claiming the global Issuer, can't find a matching participant, and returns AADSTS50068: "Signout failed. The initiating application is not a participant in the current session." The user hits an error page and the IdP session is never closed. More lenient IdPs accept the mismatched Issuer, so logout looks fine and nobody notices.
So the wrong Issuer goes out for every override connection. Strict IdPs like Entra are just where it turns into a visible logout failure.
Steps to Reproduce
- Create a SAML connection with a
samlAudienceOverride (e.g. https://saml.boxyhq.com/custom-sp-entity-id), registered at the IdP as the SP entity ID / identifier.
- Log in via SSO. The AuthnRequest
<saml:Issuer> is the override, and the IdP records it as the session participant.
- Trigger SLO through
LogoutController.createRequest.
- Decode the generated
LogoutRequest. Its <saml:Issuer> is the global samlAudience, not the override.
- Against a strict IdP (Entra/Azure) the logout is rejected with
AADSTS50068 and the IdP session stays open. Against a lenient IdP the mismatch is accepted silently.
Expected: the logout Issuer should match the one used at login (the override), so the IdP recognises the participant and completes SLO. You can check this directly on the emitted <saml:Issuer> without needing a specific IdP.
Technical details
- Root cause:
npm/src/controller/logout.ts, createRequest sets providerName: this.opts.samlAudience!, which becomes the <saml:Issuer>. It never reads samlAudienceOverride, even though the connection is loaded a few lines above.
- Login path for comparison:
npm/src/controller/sso-handler.ts uses connection.samlAudienceOverride ?? samlAudience (same pattern on the OAuth path in oauth.ts).
- The
LogoutRequest also leaves out <samlp:SessionIndex>. That isn't what's failing here (the Issuer mismatch fails first), but some strict IdPs use SessionIndex to pick the exact session, so it may be worth a look once the Issuer is fixed.
- Node.js version: 22.x, reproduced locally against the
npm test suite.
- Reproduced with a unit test that inflates the
LogoutRequest and asserts the <saml:Issuer>.
Proposed fix
I've opened #4075 with a fix. It makes logout read the override with the same fallback the login path uses:
providerName: samlAudienceOverride ?? this.opts.samlAudience!,
It also adds the optional samlAudienceOverride field to SAMLConnection, plus two tests: with an override the logout Issuer is the override, and without one it falls back to the global samlAudience (no regression). Connections without an override produce the same output as today.
Issue Summary
When Polis builds the SAML
LogoutRequest, it uses the globalopts.samlAudienceas the<saml:Issuer>and never reads the connection'ssamlAudienceOverride. The login path does use the override (connection.samlAudienceOverride ?? samlAudience), so any connection with asamlAudienceOverridesends one Issuer at login and a different one at logout.Whether that mismatch actually breaks logout comes down to how strictly the IdP checks the session participant. Microsoft Entra / Azure AD checks it strictly: it recorded the participant under the override Issuer at login, then gets a logout claiming the global Issuer, can't find a matching participant, and returns
AADSTS50068: "Signout failed. The initiating application is not a participant in the current session."The user hits an error page and the IdP session is never closed. More lenient IdPs accept the mismatched Issuer, so logout looks fine and nobody notices.So the wrong Issuer goes out for every override connection. Strict IdPs like Entra are just where it turns into a visible logout failure.
Steps to Reproduce
samlAudienceOverride(e.g.https://saml.boxyhq.com/custom-sp-entity-id), registered at the IdP as the SP entity ID / identifier.<saml:Issuer>is the override, and the IdP records it as the session participant.LogoutController.createRequest.LogoutRequest. Its<saml:Issuer>is the globalsamlAudience, not the override.AADSTS50068and the IdP session stays open. Against a lenient IdP the mismatch is accepted silently.Expected: the logout Issuer should match the one used at login (the override), so the IdP recognises the participant and completes SLO. You can check this directly on the emitted
<saml:Issuer>without needing a specific IdP.Technical details
npm/src/controller/logout.ts,createRequestsetsproviderName: this.opts.samlAudience!, which becomes the<saml:Issuer>. It never readssamlAudienceOverride, even though the connection is loaded a few lines above.npm/src/controller/sso-handler.tsusesconnection.samlAudienceOverride ?? samlAudience(same pattern on the OAuth path inoauth.ts).LogoutRequestalso leaves out<samlp:SessionIndex>. That isn't what's failing here (the Issuer mismatch fails first), but some strict IdPs use SessionIndex to pick the exact session, so it may be worth a look once the Issuer is fixed.npmtest suite.LogoutRequestand asserts the<saml:Issuer>.Proposed fix
I've opened #4075 with a fix. It makes logout read the override with the same fallback the login path uses:
It also adds the optional
samlAudienceOverridefield toSAMLConnection, plus two tests: with an override the logout Issuer is the override, and without one it falls back to the globalsamlAudience(no regression). Connections without an override produce the same output as today.