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
To perform a SP initiated SLO that terminates the session in all ServiceProviders you can combine the two flows.
This page covers the SLO implementation details. For more high-level information about SAML SLO, check out our article, The Challenge of Building SAML Single Logout.
SP-initiated SLO is fairly self-explanatory and is similar to the approach that we see with OpenID Connect. However, instead of the IdP simply returning the user to a pre-agreed endpoint, a SAML IdP returns a logout response.
The logout response will be signed using IdentityServer's signing key and can be sent using either the Redirect or POST bindings.
The Service Provider SLO endpoint to return the response to uses the following order of precedence:
- The endpoint marked as default
- The first endpoint with a Redirect binding
- Any other endpoint
Handling SP-Initiated SLO
To start SP-Initiated SLO, a valid SAML logout request must be sent to IdentityServer. This request must contain the correct Name ID (IdentityServer subject) for the currently logged in user.
SP-Initiated SLO is then facilitated by the GetLogoutCompletionUrl
method on the SAML interaction service, ISamlInteractionService
. This method will take the current SAML request ID and, if necessary, return a URL that can be used as a post-logout redirect URL.
The request ID (requestId
) is provided by the SAML component as a query string parameter and must be remembered in the logout model.
Note that the logoutId
parameter name is determined by the IdentityServer configuration option UserInteraction.LogoutIdParameter
, and the requestId
parameter name is determined by the SAML configuration option UserInteraction.RequestIdParameter
.
If you have changed the default values of these options, you will need to update the method parameters to capture the values from the web request correctly.
IdentityServer will initiate OpenID Connect or WS-Federation logout requests if a Client record contains values for FrontChannelLogoutUri
or BackChannelLogoutUri
. To prevent unnecessary requests made to SAML Service Providers, ensure that the Service Provider’s client record does not contain values for these properties.
Duende IdentityServer
See below for modified Logout and LoggedOut pages from the Duende IdentityServer QuickStart UI:
- The Logout page is modified to capture the RequestId parameter
- The LoggedOut page is modified to set a post-logout redirect URL, which is facilitated by
GetLogoutCompletionUrl
[SecurityHeaders]
[AllowAnonymous]
public class Index : PageModel
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IEventService _events;
[BindProperty]
public string LogoutId { get; set; }
[BindProperty]
public string RequestId { get; set; }
public Index(IIdentityServerInteractionService interaction, IEventService events)
{
_interaction = interaction;
_events = events;
}
public async Task<IActionResult> OnGet(string logoutId, string requestId)
{
LogoutId = logoutId;
RequestId = requestId;
// showLogoutPrompt logic removed for brevity
return await OnPost();
}
public async Task<IActionResult> OnPost()
{
if (User?.Identity.IsAuthenticated == true)
{
// create a logout context if there's not one present already
LogoutId ??= await _interaction.CreateLogoutContextAsync();
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId, requestId = RequestId });
}
}
[SecurityHeaders]
[AllowAnonymous]
public class LoggedOut : PageModel
{
private readonly IIdentityServerInteractionService _interactionService;
private readonly ISamlInteractionService _samlInteractionService;
public LoggedOutViewModel View { get; set; }
public LoggedOut(IIdentityServerInteractionService interactionService, ISamlInteractionService samlInteractionService)
{
_interactionService = interactionService;
_samlInteractionService = samlInteractionService;
}
public async Task OnGet(string logoutId, string requestId)
{
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 (requestId != null)
{
var returnUrl = await _samlInteractionService.GetLogoutCompletionUrl(requestId);
View.AutomaticRedirectAfterSignOut = true;
View.PostLogoutRedirectUri = returnUrl; // SAML logout completion endpoint
}
}
}