Friday, May 25, 2012

An unfortunate issue in the ADFS2 sign out page

In one of my last entries, I’ve described an inconsistent behavior of different web browsers which occurs when users click the Back button in their browsers.

This subtle issue causes a deadful issue in an environment built around the ADFS2.

The problem is as follows: you have your applications federated with the ADFS2, tons of them. Each application has the “SignOff” button which takes users to ADFS2. ADFS2 renders the signoff page (containing iframes with urls with wsignoutcleanup1.0) which uses a jscript to redirect to the login page (if the wreply parameter is included).

One of our clients alarmed us that signing off from applications still allows users to see the last visited page. Upon closer inspection, it turned out that because of the way Opera, Firefox and Chrome treat “redirect” pages (please consult the mentioned blog entry), clicking Back on the login page (after succesful sign off) takes users back to the last page they visited (the one they clicked “sign off” on). And because most pages are read from a disc cache, the page is rendered even though a user hasjust signed off from the application!

A disaster.

A general solution was proposed in the last blog entry. It involves a modified javascript where the redirect is not done in an explicit way but rather with a setTimeout which prevents the unintended browser behavior.

Luckily, when ADFS2 is deployed, web pages have both *.aspx and *.cs available for modifications. There is a chance then to change the way the script is generated in the SignOut.aspx page.

The offensive javascript, generated in the SignOut.aspx is as follows:

<script>
   window.onload = function() 
   { 
      document.location = decodeURIComponent('return_url_herex') 
   }
</script>

What you have to find out is to find a way to replace this script with the modified one.

When you open the SignOut.aspx.cs however, you will find out that there’s nothing much to do there:

//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------
 
using System;
 
using Microsoft.IdentityServer.Web.UI;
 
/// <summary>
/// This page sends WS-Federation SignOutCleanup messages to all the relying parties
/// that the user has signed in to during this session.
/// </summary>
public partial class SignOut : SignOutPage
{
    protected void Page_Load( object sender, EventArgs e )
    {
        CleanupUrisRepeater.DataSource = SignOutMessages;
        CleanupUrisRepeater.DataBind();
    }
}

There does not seem to be an explicit way to change the way the unfortunate javascript is generated. The base class, SignOutPage, is one of the ADFS2 core classes, it comes from Microsoft.IdentityServer.Web.UI library. If you decompile the class, it seems that it registers the startup script in the constructor.

The critical detail to change is then to replace the javascript with a new one which would use the setTimeout. Since you cannot modify the SignOutPage class and its constructor (would have to decompile, modify, recompile and resign the DLL!), my approach is as follows – I introduce a new class, CustomSignOutPage, which inherits from SignOutPage. The SignOut page class will then inherit from the newly introduced one.

Since I cannot replace the base class’ constructor – my new class will always call it. That’s unfortunate as the script is registered in the constructor, using ClientScript.RegisterStartupScript. But the ClientScriptManager class lacks a way to unregister scripts! Another unfortunate issue!

What I can do however, is to generate a new window.onload handler which will replace the one created by the script registered with an existing, base class code. The intention is to have the following structure of scripts in the SignOut page:

<!-- this one cannot be replaced because of the design! unfortunate! !-->
<script>
   1:  
   2:    window.onload = function() 
   3:    { 
   4:       document.location = decodeURIComponent('return_url_here') 
   5:    }
</script>
<!-- these must be injected !-->
<script>
   1:  
   2:    window.onload = function() 
   3:    { 
   4:       setTimeout( "location.href = decodeURIComponent('return_url_here')", 10 ) 
   5:    }
</script>
<script>
   1:  
   2:    window.onunload = function(){};
   3:    window.history.navigationMode = "compatible";
   4:    window.history.go(1);
</script>

My proposal is then to change the SignOut.aspx.cs to:

//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------
 
using System;
 
using Microsoft.IdentityServer.Web.UI;
 
/// <summary>
/// This page sends WS-Federation SignOutCleanup messages to all the relying parties
/// that the user has signed in to during this session.
/// </summary>
public partial class SignOut : CustomSignOutPage
{
    protected void Page_Load( object sender, EventArgs e )
    {
        CleanupUrisRepeater.DataSource = SignOutMessages;
        CleanupUrisRepeater.DataBind();
    }
}
 
public class CustomSignOutPage : SignOutPage
{
    protected override void OnPreRender( EventArgs e )
    {
        base.OnPreRender( e );
 
        if ( this.ClientScript.IsStartupScriptRegistered( this.GetType(), "SignoutPage" ) )
        {
            Type FederationPassiveAuth = 
                 typeof( SignOutPage )
                      .Assembly
                      .GetType( "Microsoft.IdentityServer.Web.FederationPassiveAuthentication" );
            string signOutReplyUrlFromRequest = 
                 (string)FederationPassiveAuth
                      .GetMethod( "GetSignOutReplyUrlFromRequest" )
                      .Invoke( FederationPassiveAuth, null ); 
            
             this.ClientScript.RegisterStartupScript( 
                typeof( SignOutPage ), 
               "SignoutPageFix", 
                string.Format( 
                    System.Globalization.CultureInfo.InvariantCulture, 
  "<script>window.onload = function() {{ " +
  "setTimeout( \"location.href = decodeURIComponent('{0}')\", 10 ) }}</script>", 
                        new object[]
                    {
                        JSEncode(signOutReplyUrlFromRequest)
                    }));
        }
 
        this.ClientScript.RegisterStartupScript( 
            this.GetType(), 
            "RedirectBackFix", 
              "<script>window.onunload = function(){};" +
              "window.history.navigationMode = \"compatible\";" +
              "window.history.go(1);</script>" );
 
    }
 
    public static string JSEncode(string originalValue)
    {
      if (string.IsNullOrEmpty(originalValue))
      {
          return string.Empty;
      }
      return System.Web.HttpUtility
            .UrlEncode(originalValue)
            .Replace("+", "%20").Replace("'", "%27");
    }
}

There are few things to explain.

First as you can see, I introduce a new class between the core SignOutPage class and the actual SignOut page class. The introduced class creates two scripts and injects them to the generated web page. The first script is rendered using a reflection on the FederationPassiveAuthentication type – this is because the type is internal in the ADFS2 core libraries. The second script uses the JSEncode method which is rewritten to mimic the way ADFS2 implements it.

And this is it. This does the trick and solves the unfortunate issue. Newly introduced scripts prevents the application page to be seen when users press the Back button in their browsers.

Once again – for this to work, you have to copy the above SignOut.aspx.cs and replace the original one. Remember to save a backup copy of the original file. Also, altough the solution was tested in a development environment, I can’t be 100% sure that there are no unexpected side effects of this change.

No comments: