As I get into more and more details on WIF and all the WS-Federation related stuff, I’ve been extremely interested in how single sign out should work. While single sign-on is easy, the Identity Provider Security Token Service (IP-STS) maintains its own session (probably protected with Forms Authentication or similar mechanism) and responds to wasignin1.0 requests from federated applications (Relying Parties), the single sign-out should somehow involve sending requests to each single authenticated Relying Party to inform it that the user is about to sign out of the Identity Provider.
Unfortunately, the STS template generated by Visual Studio when you “Add STS Reference” and “Create new STS” is not very helpful. What it does it signs you out from a single RP:
else if ( action == WSFederationConstants.Actions.SignOut )
{ // Process signout request.
SignOutRequestMessage requestMessage =
(SignOutRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
FederatedPassiveSecurityTokenServiceOperations.ProcessSignOutRequest(
requestMessage,
User,
requestMessage.Reply,
Response );
Fortunately, there’s this fantastic book by Vittorio Bertocci, Programming Windows Identity Foundation, which covers WIF deeply while being very clear and understandable at the same time. Vittorio explains that the RP-side modules (FAM & SAM, responsible for negotiating and maintaining a session between the RP and the IP) respond to wsignoutcleanup1.0 message appended to any request to an RP page and the result of such request would be:
a) to destroy the session at the RP’s side
b) to return an image of a green check mark image to mark a successfull operation
What a handy idea! The sign-out from multie RPs, according to Vittorio, is then:
a) to keep track of consecutive RPs signing in via the IP at the IP side, Vittorio suggests that this information could be persisted in a cookie to save the IP’s resources but probably the session could also be used
b) to return a page containing multiple images with src attribute pointing to consecutive RPs with wsignoutcleanup1.0 when wasignout1.0 is requested from the IP (Vittorio’s book, page 121)
else if ( action == WSFederationConstants.Actions.SignOut )
{ // Process signout request.
SignOutRequestMessage requestMessage =
(SignOutRequestMessage)WSFederationMessage.CreateFromUri( Request.Url );
FederatedPassiveSecurityTokenServiceOperations.ProcessSignOutRequest(
requestMessage,
User,
null, // note the null here which does not force the redirect to the login page
Response );
string[] signedInUrls = SingleSignOnManager.SignOut();
lblSignoutText.Visible = true;
foreach ( string url in signedInUrls )
{ SignOutLinks.Controls.Add(
new LiteralControl( string.Format(
"<p><a href='{0}'>{0}</a> <img src='{0}?wa=wsignoutcleanup1.0' " + "title='Signout request: {0}?wa=wsignoutcleanup1.0'/></p>", url ) ) ); }
}
Unfortunately, few details are missing here and this is the motivation of this entry.
First, it’s not possible to get a complete solution containing all the files. A link at O’Reilly site, which should probably point to book’s code samples, points to the WIF SDK which does not contain this particular example.
I’ve decided then to provide an implementation of this SingleSignOnManager class, which contains two methods. The SignOut is used in the above snippet to get the list of all RPs authenticated during the current user session. Another method, RegisterRP is used in the GetOutputClaimsIdentity of a SecurityTokenService at the IP’s side to store the information of the RP which authenticates via the IP:
/// <summary>
/// This method returns the claims to be issued in the token.
/// </summary>
/// <param name="principal">The caller's principal.</param>
/// <param name="request">The incoming RST, can be used to obtain addtional information.</param>
/// <param name="scope">The scope information corresponding to this request.</param>
/// <exception cref="ArgumentNullException">If 'principal' parameter is null.</exception>
/// <returns>The outgoing claimsIdentity to be included in the issued token.</returns>
protected override IClaimsIdentity GetOutputClaimsIdentity(
IClaimsPrincipal principal,
RequestSecurityToken request, Scope scope )
{ if ( null == principal )
{ throw new ArgumentNullException( "principal" );
}
// remember all the RPs to be able to perform a single sign-out
SingleSignOnManager.RegisterRP( scope.ReplyToAddress );
ClaimsIdentity outputIdentity = new ClaimsIdentity();
// Issue custom claims.
// TODO: Change the claims below to issue custom claims required by your application.
// Update the application's configuration file too to reflect new claims requirement.
...
return outputIdentity;
}
The SingleSignOnManager would be as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace WebSTS
{ public class SingleSignOnManager
{ const string SITECOOKIENAME = "StsSiteCookie";
const string SITENAME = "StsSite";
/// <summary>
/// Returns a list of sites the user is logged in via the STS
/// </summary>
/// <returns></returns>
public static string[] SignOut()
{ if ( HttpContext.Current != null &&
HttpContext.Current.Request != null &&
HttpContext.Current.Request.Cookies != null
)
{ HttpCookie siteCookie =
HttpContext.Current.Request.Cookies[SITECOOKIENAME];
if ( siteCookie != null )
return siteCookie.Values.GetValues(SITENAME);
}
return new string[0];
}
public static void RegisterRP( string SiteUrl )
{ if ( HttpContext.Current != null &&
HttpContext.Current.Request != null &&
HttpContext.Current.Request.Cookies != null
)
{ // get an existing cookie or create a new one
HttpCookie siteCookie =
HttpContext.Current.Request.Cookies[SITECOOKIENAME];
if ( siteCookie == null )
siteCookie = new HttpCookie( SITECOOKIENAME );
siteCookie.Values.Add( SITENAME, SiteUrl );
HttpContext.Current.Response.AppendCookie( siteCookie );
}
}
}
}
As you can see, each consecutive RP is added to the list of RPs remembered in a cookie created by the IP. The only possible drawback of this implementation is that there’s a chance that the cookie will overgrow the legal limit of a cookie size.
The last but not least detail is to add two controls to the IP’s authentication page (Default.aspx in the template example), a label lblSignoutText and a panel SignoutLinks.
When you build your custom test environment consisting of a IP-STS and two (or more) RPs, as the result of a sign-out (the easiest way to trigger the sign-out is to use the wif:FederatedPassiveSignInStatus control which shows a sign-out link) you’ll note that signing out no loger incorrectly redirects to the login page in a context of a single RP (leaving all other RPs logged in!) but rather it shows a page with links to consecutive RPs and images of green check marks next to all links to indicate the succesfull sign-out.
A next step of my research is to build a “virtualized” IP-STS, which serves as the IP for different data sources but from a single web application.