Yossi Dahan [BizTalk]

Google
 

Monday, June 22, 2009

Implementing Single Sign Out scenario with the Geneva Framework

One of the items on my to-do list for a while now was to add support for single sign out in our passive scenarios; the idea is that if a user browses to several RPs, and then hits the sign out button on one of them, she would automatically be sign out of all the other RPs visited in this session.

Whilst, as you will see shortly, the framework has great support for this scenario, and it is easily achieved, it is not the out-of-the box behaviour; out of the box – if you’re using the SignInStatus control (with or without the FAM) and the FederatedPassiveTokenService control, when the user hits the sign-out button of the SignInStatus control, she will be signed out of the current RP, as well as the STS itself, but any other RPs the user had visited in this session will keep her logged in.

So – if the user browsed to application A, authenticated at the STS, and then browsed to application B, she is not signed on in both applications as well as on the STS; hitting the sign out button in application B will sign her out of application B as well as of the STS; if she tries to browse to application B now (with no browser caching), she will get redirected to the STS, and would need to re-authenticate there; same would happen if she tried to browse to any application other than application A, which is protected by the STS; within application A, however, the user would still be authenticated and she will be able to keep using this app.

In some cases this may be acceptable, but in our case the users assume that if they hit sign out, they are signed out of the entire “set”, and so we were set on achieving this behaviour.

It turns out that the framework has great support for this scenario, and that only very little code is required to achieve this; in fact – on the RP side – there’s nothing to do.

Both the FAM and the SignInStatus controls handle requests to sign off out of the box, all you have to do is send an HTTP request with “wa=wsignoutcleanup1.0” in the query string and the framework will take care of removing the local cookies; it will even return a nice image to indicate success (you can control which image to show through configuration);

To see this in action – create a standard scenario with two RPs configured to use a single STS; add to your STS an ASPX page,  which would look something like this (you will need to update the urls to point at your RPs) -

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SignOut.aspx.cs" 
Inherits="HRG.Profile.Identity.STS.Web.Passive.SignOut" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<
html xmlns="http://www.w3.org/1999/xhtml" >
<
body>
<
form id="form1" runat="server">
<
img src="https://localhost/ststests/testwebsite4/default.aspx?wa=wsignoutcleanup1.0" /><br />
<
img src="https://localhost/ststests/testwebsite3/default.aspx?wa=wsignoutcleanup1.0" />
</
form>
</
body>
</
html>



In the code behind add the following -



protected void Page_Load(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
}


Now run through your scenario - login to one application, then browse to another, then browse to this test page; you should see a couple of “green ticks” indicating you have been signed out of both applications.



Now try to browse to either of them (make sure to refresh to pages to avoid browser caching) - you should notice that you’re no longer authenticated in neither (nor the on the STS) and that your’e redirected to the STS’ login page. cool!



So- we’ve proved that there’s really nothing to do on the RP side to achieve single sign out; but what do we need to do on the STS side? well – when the user hits the sign out link button on the SignInStatus control a request goes to the configured url for the STS, so this would be the entry point; what we really need to do is figure out a way to, for example, dynamically generate a page similar to our test page above; to do that we need to be able to a) track the RPs a user had visited and b) control the behaviour of the STS when the user hits sign-out on any RP, to make the required sign out requests to all the other RPs.



Until now I’ve been using the STS control (FederatedPassiveTokenService) in my passive STS, and so – to add behaviour required I would have to extend it, which is not something I felt comfortable doing; the alternative was to get rid of the control altogether and simply write the code required to handle both sign in and sign out from scratch, which is something I wanted to experiment with (in fact – I had to do much of it it in a different area of my solution – bridging single sign out protocols, but that’s for another post), so I though this is a good opportunity to give it a go.



As it turns out, as the framework has all the code to do the heavy lifting, all I needed to do is “control the flow”, and it was all relatively painless - I removed the controls from my page, and started to replace it by placing code in the code behind -



First – I needed to figure out what request I’ve received from the caller; this was as simple as two lines -



WSFederationMessage message = null;
bool messageCreated = WSFederationMessage.TryCreateFromUri(Request.Url, out message);



messageCreated now indicates whether the request to the STS was a valid one, message is expected to be either SignInRequestMessage or SignOutRequestMessage (there are two other possible request types that are not currently supported by the framework, but that’s for another day)



Before I’ll go back to my single sign out scenario, I need to complete the single sign in scenario as I no longer have the control on the page (I could, potentially, leave the control on the page and do nothing if the message was a sign in request – leaving the control to do all the work, and if the message was a single sign out request execute whatever code I needed to achieve that, but I wanted to get rid of the control so I implemented code for both paths)



So – to implement the single sign in my STS needed to call the STS, get a SignInResponse message and write that to the Http response stream, how is this done? well – there may be many favours, but the main code would look something like this (some elements removed for bravity) -



if (message is SignInRequestMessage)
{


   SignInRequestMessage requestMessage = message as SignInRequestMessage;



// Create our STS backend
SecurityTokenService sts = new MySTS(stsConfig);

// Create the WS-Federation serializer to process the request and create the response
WSFederationSerializer federationSerializer = new WSFederationSerializer();
WSTrustSerializationContext serialisationContext = new WSTrustSerializationContext();
// Create RST from the request
RequestSecurityToken request = federationSerializer.CreateRequest(requestMessage, serialisationContext);

// Get RSTR from our STS backend,
//the thread's principal would not be an IClaimsPrincipal, so create one from the contained identity
IClaimsPrincipal principal = ClaimsPrincipal.CreateFromIdentity(Thread.CurrentPrincipal.Identity);
//issue the RSTR
RequestSecurityTokenResponse response = sts.Issue(principal, request);
// Create Response message from the RSTR
SignInResponseMessage response = new SignInResponseMessage(new Uri(response.ReplyTo),
response,
federationSerializer,
serialisationContext);



response.Write(Page.Response.Output);
Response.Flush();
Response.End();



}



I’m creating an instance of the STS for each request, but am re-using the sts configuration class (which I keep as an “Application” variable in the STS’ asp.net application).



I’m then using a federation serialiser to create the RST, run this through the STS (providing the principal, “converted” to a ClaimsPrincipal) and then create a SignInResponseMessage out of the RSTR returned by the STS;



Job done – my STS now supports single sign in without the control;



You can already imagine what I needed to do to complete the sign out support- to start with I needed to add an else-if to handle SignOutRequestMessage (as I’ve mentioned – there are other types of requests theoretically possible, but lets not worry about them at the moment) -



else if (message is SignOutRequestMessage)
{


}



The first thing I would do there, is sign out the user from the STS itslef -



FormsAuthentication.SignOut();



All I needed to do now is add bog standard ASP.net code to generate the required Http Get requests to all my RPs; but to do this I needed to keep track of which RPs a user had visited within the session, so I know which RP’s to sign her out of; to help me achieve that I’ve created the following class to track the user’s visited realms -



public class VisitedRealmsTracker
{
private Dictionary<string, string> visitedRealms = new Dictionary<string, string>();

public void Add(string sessionId, string realm)
{
string key = sessionId + "|" + realm;
lock (visitedRealms)
{
if (!visitedRealms.ContainsKey(key))
visitedRealms.Add(key, sessionId);
}
}

public IEnumerable<string> GetAllRealmsForSession(string sessionId)
{
//find all visited realms for this session and return the second part of they key (after the '|') which would be the realm
return from visitedRealm
in visitedRealms
where visitedRealm.Value == sessionId
select visitedRealm.Key.Split('|')[1];
}

public void ClearUserEntries(string sessionId)
{
lock (visitedRealms)
{
List<string> keys =
visitedRealms.Where(realm => realm.Value == sessionId)
.Select(realm=>realm.Key).ToList();
foreach (string key in keys)
visitedRealms.Remove(key);
}
}
}


I’ve added a member of this type to my STS Configuration class, which – you would remember – I now keep as an application variable, and so I could add a call to Add in the STS code handling the sign in request I showed earlier, which would ensure I’m tracking all the visited realms; the sign out logic could now iterate over the results of the GetAllRealmsForSession and handle the logout requests, simple code to achieve this could be something like -



foreach(string realm in stsConfig.Tracker.GetAllRealmsForSession(Session.SessionID))
{
//get realm configuration
ReliantPartyConfigurationElement rpConfig = Configuration.ReliantParties[realm];
//create an image pointing the at realm's signout url appending the signout and cleanup action
Image img = new Image();
img.ImageUrl = rpConfig.SignOutUrl.Trim()+"?wa=wsignoutcleanup1.0";
Repeater1.Controls.Add(img);
//add a line break after each realm
LiteralControl br = new LiteralControl("<br />");
Repeater1.Controls.Add(br);
}



This sample code simply creates the same images I’ve previously had hard coded in the test page dynamically.



With all the code in place – sign in requests are being processed by code instead of the control, with the code now customised to keep track of visited realms, sign-out requests use this tracked information to dynamically build a page that issues the required Http get request to all the visited RPs to sign the use out of all of them; single sign out achieved. easily.

Labels: ,

7 Comments:

  • Good stuff indeed, but I'm having trouble locating the references (using Beta 2) for this line:

    ReliantPartyConfigurationElement rpConfig = Configuration.ReliantParties[realm];

    Could you please help me out, thanks?

    By Blogger Jay, at 27/08/2009, 19:01  

  • Obviously the blog post only contains snippets of code, and not the entire solution, which - unfortunately - I cannot publish.

    I have implemented a set of configuration classes to provide the STS information about the reliant parties; all that is needed at this point is the Url to the RP, so that the required query string can be appended to it.

    Naturally you can replace this piece with any method you wish to get that url.

    I hope that makes sense

    By Blogger Yossi Dahan, at 27/08/2009, 20:22  

  • Good, I have a question, can I signout without cliking ont SignOut Button of the SignOutStatus and without adding SignOutStatus in my Page. I would like to do it programmatically.
    Thanks

    By Anonymous Anonymous, at 03/12/2009, 11:18  

  • Short answer is yes.
    As you can see in the code in the post, the signout request to each site is effectively an HTTP get, right?
    In my single-sign-out page I'm just rendering html with images, each going to the relevant site asking it to signout, you can easily do it with code.
    From the STS itself its a standard signout request, so that's easy as well.

    Does that make sense?

    By Blogger Yossi Dahan, at 03/12/2009, 14:04  

  • Thanks, But my problem is that I want to delete these two lines of code from my pages :
    "< % @ Register Assembly="Microsoft.IdentityModel, Version=0.6.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Namespace="Microsoft.IdentityModel.Web.Controls" TagPrefix="idfx" %>"
    and "< idfx : SignInStatus ID="SignInStatus1" runat="server" AutoSignOut="true" />"

    Then after the signoutcleanup the user still authenticated in the RP and in the FP.
    Is there a method to change these two lines by a C# program.
    Thanks

    By Anonymous Anonymous, at 03/12/2009, 15:43  

  • Apologies, I'm unable to look into the details at the moment, have you looked at - http://social.msdn.microsoft.com/Forums/en/Geneva/thread/bdc7c38f-3097-4627-9f38-553c686727e8

    Does this guide you in the right direction?

    If you want to do it programatically I'm 100% sure you can, but you will need to write the code already in the control, so I'm not sure why that's a good idea?

    By Blogger Yossi Dahan, at 03/12/2009, 16:33  

  • Exactly what I need, I must add these lines :
    FederatedAuthentication.WSFederationAuthenticationModule.SignOut();
    FederatedAuthentication.WSFederationAuthenticationModule.SignOut(true);
    FederatedAuthentication.SessionAuthenticationModule.SignOut();
    if (Thread.CurrentPrincipal is IClaimsPrincipal)
    {
    ClaimsIdentityCollection claims = ((IClaimsPrincipal)Thread.CurrentPrincipal).Identities;
    claims.Clear();
    claims.Add(new ClaimsIdentity(new Claim(string.Empty, string.Empty)));
    }
    m_Page.Session.Clear();
    m_Page.Session.Abandon();

    Thanks

    By Anonymous Anonymous, at 04/12/2009, 10:15  

Post a Comment

<< Home