Identify SLO - New behaviors
SSO & SLO
Single sign-on (SSO) allows a user to sign on with one set of credentials and at once gain access to multiple Service Providers (SP). Given the user has logged in to multiple Service Providers using one or more Identity Providers (aka "participants"), when the user logs out of one participant, the expected behavior is that the user is also logged out of all other participants (SLO - Single Log Out). In order to do that, Identify must maintain a list of all SSO participants for the current logged-on session.
A new way to manage SSO participants
Previously, Identify stored the participant list in either cookies or in the SQL database and maintained other information in an ASP.Net session state. However, there are some limitations with that approach at SLO-time, and we have received many requests from our customers to improve the SLO experience. Therefore, we have redesigned the SLO module. Firstly, all participants in the same SSO session are stored in a browser's cookies now. Secondly, we encrypt all participant cookies where relevant to avoid leaking users' personal data. Thirdly, we have revamped how Identify handles SLO requests behind the scene as to make it more resilient to errors. The two key advantages of this approach are that Identify can avoid using its database for storing participants, which saves cost as well as speed up the performance, and it no longer suffers from the session expiration issue.
After a user finishes a login with Identify, you may notice a new HttpOnly cookie with the participants prefix:
The cookie contains all participant information which Identify uses for doing SLO.
Supporting single log-out for multiple protocols
Because Safewhere Identify supports different protocols like SAML 2.0, WS Federation, and OAuth 2.0/OIDC, a log-on session can potentially have participants from all three protocols. With SLO, your users expect to log out from a single participant as well as be automatically logged out from all other participants. The challenge is that each protocol has its own recommendation for how SLO should be done.
- The SAML 2.0 protocol, especially the Kantara Initiative's profile and the OIOSAML profile, favors logging out participants one by one without using any HTML frame. For every LogoutRequest, the sender expects to receive a LogoutResponse.
- The WSFederation specification states in the section 13.1.2 that "They could be effectively chained through all the STSs involved in the session by successively redirecting the browser between each resource IP/STS and the requestor’s IP/STS. Or the requestor’s IP/STS can send sign-out messages to all the other STSs in parallel. The chained (sequential) approach has been found to be fragile in practice. If a resource IP/STS fails to redirect the user after cleaning up local state, or the network partitions, the sign-out notification will not reach all the resource IP/STSs involved. For this reason, compliant implementations SHOULD employ the parallel approach." In other words, it supports the use of the parallel approach.
- The OIDC 2.0 session management profile defines how Identify can log OIDC client applications out by using iframes, a cookie, and postMessage.
Therefore, Identify treats participants of each protocol differently:
- SAML 2.0: The logout process is done sequentially (also called synchronously), i.e. Identify logs SAML 2.0 participants out one by one. SAML 2.0 participants are always logged out before those of the other protocols.
- WSFederation: Identify logs WSFederation participants out in parallel in iframes (aka asynchronously).
- OIDC: Identify logs OIDC participants out per described in the Session management specification.
SSO context
Before we go to explore how an SLO flow works, let's have a look at what an SSO context is. Given that you have the following setup in Identify:
- SP1 can log in using IdP1.
- SP2 can log in using IdP1.
- SP3 can log in using IdP2.
If a user logs in to all those 3 SPs, the federation of {SP1, SP2, IdP1} forms one SSO context and the federation of {SP3, IdP2} forms another SSO context. When the user logs out of SP1, SLO is triggered for SP2 and IdP1 as well. However, the SSO context of {SP3, IdP2} remains intact.
Favoring completeness over correctness
Previously, Identify expected that everything would be in ideal conditions for SLO to complete: session state must not have expired and all participants (regardless of protocols) must have responded to logout requests from Identify successfully. If something went wrong, the SLO flow would break. Identify is now designed to be more resilient to errors: if it detects that something is not as expected, e.g. a WSFederation participant fails to respond or a SAML 2.0 participant returns a failed response, it will proceed to log other participants out. Comparing the two approaches, the new one obviously has more participants logged out and thus can be considered more secure.
How the SLO process works
Identify can receive a logout request from one of three Service Provider types: SAML, WS Fed or OAuth2.0/OIDC, or from an upstream Identity Provider, the common logout flow as following:
In general, Identify processes SLO as follows:
- All SSO processes start with Identify receiving a logout request from a participant. This can be a SAML 2.0 LogoutRequest request, a WSFederation SignOut or SignOutCleanUp request, or an OIDC logout request.
- Identify extracts relevant information from a request, e.g. an Entity identifier if that is a SAML 2.0 LogoutRequest request to find out who the requester is and its participant entry in the participant list. From there, Identify also finds the SSO context and what participants to log out.
- For each SAML 2.0 participant (if any) in the SSO context, Identify logs them out one by one:
- Send a LogoutRequest to the participant, remove it from the participant list, and write it to a special cookie that keeps track of the participant that is being logged out.
- The participant either returns a LogoutResponse or fails to return because of some error happening on its side. If the latter happens, the SLO unfortunately breaks. If the former happens, it proceeds to the next step.
- Identify checks if there is another SAML 2.0 participant to log out. If there is, it repeats the flow from step 1 until all participants are processed.
- Note that if Identify receives a LogoutResponse with a non-success status, or if the response has a wrong signature, Identify will log a warning message, mark the SLO session as "Partial logout" and continue.
- Render a page to respond to the requester in step 1 with an iframe.
- The iframe requests the "/asynchronouslogout" endpoint to log all the WS Federation and OIDC participants in the same SSO context out.
- The page will submit a response to the requester after the iframe finishes loading.
- The "/asynchronouslogout" endpoint renders multiple iframes, one for each WSFederation participant (if there are) in the SSO context, to log out in an asynchronous manner.
- If there are any OIDC participants in the SSO context, it logs them out and switches the status cookie value to "signedoff". If an OIDC client implements session management correctly, it will detect the change in status and clean up its session accordingly.
Below is an example of a scenario where the SSO context has 2 SAML SPs (sp1.safewhere.local and sp2.safewhere.local), a WSFederation SP (claimapp.safewhere.local) and an OIDC SP (oidc1.safewhere.local). One of SAML SP initiates an SLO:
- The SP1 started the flow by sending a SAML LogoutRequest to the Identify instance (dev.safewhere.local).
- The Identify instance received the request and logged the SP2 out from row 2 to 5.
- At row 5, the Identify instance rendered a response to the SP1 and rendered the "/asynchronouslogout" endpoint into an iframe to log all WSFed and OAuth2.0/OIDC SPs out from row 6 to 10.
- After all participants were logged out, the page at step 3 made a form post to submit the response to the request's initiator (the SP1) to finish the SLO flow.
- Because the OIDC client supports the session management feature (from row 13), after receiving a signal about status change from the Identify instance, it sent a logout request to Identify (row 17). On the Identify side, because there was no SP to log out anymore, it just made a response back to the client's logout callback URL.
As a side note, the new SLO design works when you performs a logout from the Identify's IdpInitiated page as well.
Logout candidate matching
In the above SLO processing, Safewhere Identify needs to determine a logout candidate (step 2) based on the received logout request. By default, Identify uses the SessionIndex from the logout request to find a candidate from the participant list. Additionally, Identify offers the option to use NameID matching for SAML and OIDC candidates as follows:
SAML
This option is unchecked by default on both SAML Service Provider and SAML Identity Provider connections. In this scenario, Identify operates as follows:
- It uses the SessionIndex to locate the logout candidate.
- If no matching candidate is found, Identify will attempt to use the NameID for matching.
If the option is checked, Identify will exclusively rely on the NameID for finding the logout candidate. In this instance, you must use the NameID claim transformation to issue a NameID to the corresponding Service Provider connection.
If a logout candidate is identified through the matching of NameID, and the logout request includes a SessionIndex, it is imperative that the SessionIndex in the logout candidate aligns with the one provided in the logout request.
OIDC
Similar to the SAML connection, on the OIDC connection there is an option named "Use 'sub' claim to match logout request with authentication session":
By default, the setting is unchecked, the matching is based on the Client ID. When the option "Use 'sub' claim to match logout request with authentication session" is checked. Finding the logout candidate will work as flow:
- Extract the
sub
and SessionIndex (uiId
) from theid_token_hint
parameter - Do the matching on both these information against the participant's NameID and SessionIndex to find the logout candidate from the login participants.
Please be aware that a NameID claim transformation is necessary for the associated Service Provider connection, similar to the requirement for an SAML connection.
System claims, In claim principal and Out claims principal
A login in Identify involves 3 actors: a Service Provider, Identify, and an Identity Provider (including Username & password). Identify receives an In claims principal from an Identity Provider, runs it through claims transformations, and the output is an *Out claims principal that it then sends to the Service Provider.
In addition to standard claims, Identify now stores some more internal claims in both claims principals that it needs to use internally:
- NameIdIn (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier/in): holds value of the NameId returned from an upstream Identity Provider.
- NameIdOut (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier/out): holds value of the NameId that Identify is going to send to a Service Provider.
- System claim (urn:safewhere:system): contains a many flags that Identify uses to when processing a login such as SessionIndex, second factor status.
Those internal claims are only used by Identify internally. They are neither issued to Service Providers nor logged. However, when you develop custom extensions, e.g. a transformation to log claims to a custom store or filter claims, you can find those internal claims. A good practice is to ignore them: do not log them, do not remove them.
Logout Service Provider individually instead of SLO
The SLO functionality is enabled by default. However, there are situations where a Service Provider may require individual logouts for specific business needs. To achieve this, follow these steps:
- Create a scripting claim transformation to append the "NOTSLO" flag ("urn:safewhere:not:slo") to the System claim.
if (Exist("urn:safewhere:system")) {
var systemClaim = ClaimsPrincipal.Claims.FirstOrDefault(c => c.Type == "urn:safewhere:system");
systemClaim.Properties.Add("urn:safewhere:not:slo", "true");
}
- Apply the claim transformation to the connection of the relevant Service Provider.
By applying the claim transformation to the Service Provider connection, when the Service Provider sends a logout request to Identify, Identify only logs it out. The login sessions of other Service Providers in the same browser session will remain unaffected.
It's important to note that logging out a "NOTSLO" Service Provider does not trigger a logout request to the upstream Identity Provider.
Known limitations
Because the new design heavily depends on the browser's cookies, it has some limitations due to how browsers and web servers restrict cookie size.
According to the HTTP State Management Mechanism specification, web browsers must support a minimum of:
- 20 cookies per domain
- 4096 bytes per cookie
The minimum size that a browser must support is 20x4096 = 81920 bytes, which should be large enough for Identify to store required information for SSO/SLO. However, the default request header size limitation in a Windows system is 16KB per https://docs.microsoft.com/en-us/troubleshoot/iis/httpsys-registry-windows. If the request header size is larger than that number, the IIS will return a bad request error with the error message: The size of the request headers is too long. Fortunately, we can change the MaxRequestBytes value on the host to allow a bigger number to avoid this kind of error.
The fact that Identify encrypts the participant cookie makes its size significantly longer. The below table has an estimate for how many participants that a user can log in to in a browser session in 3 different values of the MaxRequestBytes value:
By default, there can be up to about 20 participants in a browser session, which is deemed to meet most scenarios. In some extreme cases, where you want to allow more participants, you can consider tuning the MaxRequestBytes value to fit your need.
Other notes
- Identify SLO implementation works per browser session. If a user opens two independent browser sessions (e.g. a Chrome one and Firefox one, or a normal one and a private one), only the session for the browser that initiates the logout is terminated. The session in the other browser is still active.
- Azure AD log out endpoint version: from the v2, for security purposes, the Azure AD logout endpoint behavior has been changed to show the account selection screen so that users can select an account to log out from. For more information: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/126. This means that by doing a request to logout endpoint it does not log out the current user automatically. As a result, we are sticking with using version 1.
- When using Chrome's Incognito mode, if the "Block third-party cookies in Incognito" option is checked, SLO requests to Azure AD will not contain any cookies. As a result, Azure AD sessions are not logged out. This is a limitation of how Azure AD handles logout requests.