Wednesday, December 1, 2010

WIF, WS-Federation and Single Sign-out from Multiple Relying Parties

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>&nbsp;<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.

13 comments:

Ghanshyam said...

Good tip! However, all works when I have a single STS and multiple RP's. I am not seeing it work when the STS and RP is deployed in web-farms. I am currently testing with two RP's deployed in web-farm and my STS also deployed in web-farm.

In the STS logout page, both the RP's show green tick mark. Howver, the RP where the logout is clicked only signs out and Fed' cookies are not present, whereas the other RP is still signed in and I can still see that the Fed' cookies are still present.

How do I handle this? Any ideas??

Wiktor Zychla said...

I have no idea why you encounter issues. If a single RP is deployed in a web-farm and the authentication is cookie-based then, to me, it seems impossible that when the cookie is removed it is still present at the other server in the farm.

Cookies exist in your browser, so if a cookie is deleted, it's gone. There's no way for a cookie to be magically resurrected after it's deleted.

Are you sure that you do not have two different RPs which by accident share the same name for authentication cookie?

Ghanshyam said...

Hi, I am sorry but I could not understand your question:
"Are you sure that you do not have two different RPs which by accident share the same name for authentication cookie? "

My STS creates an authentication cookie based on username, the standard FormsAuthentication way. This is not specific to any RP. Coming to RP, the authentication type is set to "None". WIF has it's own cookies FedAuth, FedAuth1 etc and I happened to notice that for my two different RP's, the FedAuth cookies have diferent paths. For RP1, path would be /RP1/ and so on.

Am I missing something?

alex said...

Thanks. This article really helpful. It solves at least I found where the is root of the problem.

Does this solution work for cross domain RP?

Wiktor Zychla said...

alex, if I understand the question correctly then yes, passive WS-Federation always works across different domains.

Gaurav said...

Hi Wiktor,
Thank you very much for the tip. I am now able to signout from multiple RPs. But i am having a weird problem. When all the RPs and STS are deployed on the same server, and user tries to re login after being successfully signout from any of the RPs, he has to login twice. On the first try, it refreshes the login page and again user has to supply credentials. Second time , it successfully redirects to requested RP page.
Single-sign on and single sign out has no issue. only on the STS login page, a refresh occur first time when user tries to re-login.
This issue do not occur if i deploy each RP and STS on different server. Every thing works fine in this case.
Do you have any idea why this could occur?
Thanks

Wiktor Zychla said...

Without inspecting the code I can't say anything. I assume that we say about a custom STS (not the ADFS2 for example) and this is probably where the problem lies - a subtle problem in the implementation prevents the STS from behaving correctly.

NILESH said...

when i logout i am displayed with a browser having url parameters signout1.0. hence i cannot login back to the url.

can any one help me out in this.

Duy Pham said...

Hi Wiktor,

What's about Single-Sign-Out in the scenario that STS acts as a IdP through ADFS2.0, and ADFS2.0 is the federation provider to all RPs?

I have problem with Single-Sign-Out in this scenario. IdP (STS) does successfully sign out and also sends the wsignoutcleanup1.0 action back to ADFS2.0. But it seems that the sign out process stops at ADFS2.0.

Wiktor Zychla said...

Did you read my answer to your SO question? The trick would be to replace images with iframes.

Duy Pham said...

Thanks so much for your help with the SSO, Wiktor. Our system is working very fine with Single-Sign-On and Single-Sign-Out now.

There still is a little thing that I don't really understand: I set some custom cookies when logging out, and those cookies can be seen from the custom IdP (if I sign in using custom IdP before). However, if I sign in with ADFS (integrated authentication), my custom cookies are cleared when I try to access from SignOut.aspx (same when using Fiddler to trace).
I can see that ADFS does clear all the cookies before initiating SignOut.aspx page, so I guess it's internal implementation of ADFS.

Wiktor Zychla said...

The ADFS should never clear any custom cookies, if it does, make sure these cookies don't hit ADFS. You make it so by issuing your custom cookies for your IdP host name - the browser won't send cookies to adfs.yourdomain.com when cookies are issued from idp.yourdomain.com.

hari said...

I tried to implement single Sign-Out in my application as discussed. I wanted to use the FederatedPassiveSignInStatus control. But when i tried to implement it in one of my RP, i'm getting an error "A SessionAuthenticationModule must be added to the ASP.NET Module Pipeline"

Can you please help?

Hari