The Rock Solid Knowledge SAML IdP component supports two SAML Single Logout (SLO) flows:
- SP-initiated SLO where the SP can initiate single logout for the current session in the upstream SAML IdP
- IdP-initiated SLO where logout from the IdP initiates single logout for all parties in the current session
IdP-Initiated SLO causes the SAML IdP to call all logged-in Service Providers and inform them that the session is ending. We support two methods of notifying service providers when using IdP-initiated logout:
- iFrames
- Iterative/Traditional
The original SAML 2.0 specification details an iterative process where the IdP redirects the user to each Service Provider in turn. Complete control is forwarded to all the target service providers iteratively. This redirect chain approach is slightly old-fashioned, error-prone and not very user-friendly. The entire SLO process will fall over even if a single SP in the chain is unresponsive. The remaining SPs will never know that a failed logout attempt was made, resulting in various orphaned sessions, and the user will be displayed a generic HTTP error with no meaningful details from either the IdP or the SP.
As a result, we also offer a method inspired by the OpenID Connect front-channel logout, which uses iFrames, allowing the component to send protocol-compliant logout requests whilst keeping a consistent user experience. Using iframes to make logout requests means that the IdP can send SAML logout requests to all SPs in parallel, preventing the entire SLO chain from falling over due to one unresponsive SP.
Our component uses the iFrames approach by default.
The iterative/traditional approach must be enabled by setting UseIFramesForSlo
to false.
Although the iterative approach has drawbacks, it may become necessary due to the upcoming browser changes, which will cause the iframes to block 3rd party cookies.
For a more high-level overview of SAML SLO, check out our article, The Challenge of Building SAML Single Logout.
iFrames SLO Approach
To trigger IdP-Initiated SLO, the GetSamlSignOutFrameUrl
method on the SAML interaction service, ISamlInteractionService
, must be called using the logoutId
from the IdentityServer end session request. This will generate a URL that must be opened in an iFrame on your LoggedOut screen, much like for OpenID Connect.
See below for a modified LoggedOut OnGet
method from the Duende IdentityServer QuickStart UI:
public async Task OnGet(string logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
View = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
if (logout != null)
{
var samlLogout = await _samlInteractionService.GetSamlSignOutFrameUrl(logoutId, new SamlLogoutRequest(logout));
View.SamlSignOutIframeUrl = samlLogout;
}
}
This iFrame should then be displayed on your LoggedOut view like so:
@if (Model.View.SamlSignOutIframeUrl != null)
{
<iframe width="0" height="0" class="samlsignout" src="@Model.View.SamlSignOutIframeUrl"></iframe>
}
Iterative/Traditional SLO Approach
By default, our product will use iFrames.
To use the iterative approach, you must set the configuration option UseIFramesForSlo
to false.
builder.Services.AddIdentityServer()
.AddSamlPlugin(options =>
{
// Other configuration code removed for brevity
options.UseIFramesForSlo = false;
});
To trigger IdP-Initiated SLO, the ExecuteIterativeSlo
method on the SAML interaction service, ISamlInteractionService
, must be called using the logoutId
from the IdentityServer end session request.
This method initiates the traditional logout process.
The ExecuteIterativeSlo
method also takes a completion URL.
Once the SLO process is completed, the user will be redirected to the specified completion URL.
If you are combining this with SP initiated SLO, you can use GetLogoutCompletionUrl
as the completion URL to have the SP initiated SLO finish once the iterative SLO has completed
You can call ExecuteIterativeSlo
at any point, depending on your logic and preference in the logout flow.
Example 1 - Logout page
The following example shows the modified Logout page from the Duende IdentityServer QuickStart UI. IdentityServer will perform local sign-out, log out of all service providers iteratively, and finally redirect to the LoggedOut page.
public async Task<IActionResult> OnPost()
{
if (User?.Identity.IsAuthenticated == true)
{
LogoutId ??= await _interaction.CreateLogoutContextAsync();
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
// initiate iterative SAML-Initiated SLO
var completionUrl = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });
await _samlInteractionService.ExecuteIterativeSlo(HttpContext, LogoutId, completionUrl);
return new EmptyResult();
}
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
}
Example 2 - Combining SP and IDP initiated SLO with OIDC and SAML Clients
For a true SLO experience you can combine OIDC and SAML SP and IdP initiated SLO to have all sessions terminated when performing any kind of SLO. For this to work, IdentityServer needs to know how to handle this process when triggered by both a OIDC and SAML client.
If you are logging out of OIDC clients using iFrames on the LoggedOut page, you may wish to perform the iterative SLO at a later stage after notifying OIDC clients.
The following example shows the modified LoggedOut page from the Duende IdentityServer QuickStart UI.
Here we set the redirect of our OIDC iFrame based SLO to a dedicated SAML SLO page to ensure the SAML SLO takes place after the OIDC SLO has completed. If a SAML SP initiated the SLO request, we'll have a requestId and that needs to be passed along to the next page to retrieve the final completion path. If an OIDC client started the SLO the requestId will be null, but we still want to end any SAML sessions, so the user is still redirected to the SAML SLO page just with a null requestId.
public async Task OnGet(string logoutId, string requestId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
View = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = true,
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
// Configure iterative SAML-Initiated SLO
View.PostLogoutRedirectUri = Url.Page("/account/logout/SamlIterativeSlo", new { logoutId = logoutId, requestId = requestId });
}
You can then create the SamlIterativeSlo
page, which executes the iterative SLO process.
public async Task<IActionResult> OnGet(string logoutId, string requestId)
{
//If no logout Id the session is orphaned and no logout information exists
if (string.IsNullOrWhiteSpace(logoutId))
{
return Redirect("~/");
}
string completionUrl;
//If SLO was started by a SAML Client they should be redirected back to the SAML SLO endpoint
//to complete the original request
if (!string.IsNullOrWhiteSpace(requestId))
{
completionUrl = await _samlInteractionService.GetLogoutCompletionUrl(requestId);
}
else
{
//If no SAML Client was involved, redirect to the original post logout redirect URI once SLO is complete
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
completionUrl = logout?.PostLogoutRedirectUri;
}
await _samlInteractionService.ExecuteIterativeSlo(HttpContext, logoutId, completionUrl);
return new EmptyResult();
}
Once the iterative SLO is completed the user will either be redirect the OIDC postlogouturi, or if the original client was a SAML client doing SP initiated SLO, the SLO endpoint to complete that request
A complete sample for this flow can be found in our samples repository