Monday, March 12, 2012

Using wif:FederatedPassiveSignIn Control in ASP.NET MVC 3 applications

The great “A Guide to Claims-Based Identity and Access Control” handbook shows how to integrate WIF into a MVC application. They propose a new custom MVC filter to replace the built-in Authorize filter and they called the new filter AuthenticateAndAuthorize. The new filter is responsible for redirecting unauthenticated requests to a STS and then they need a dedicated view (FederatedResult) to get and translate the response of the STS.

I believe an easier approach exists – where you don’t have to introduce your own authorization filter and (possibly) don’t have to correct all your controllers so that they are decorated with the new AuthenticateAndAuthorize filter instead of the existing Authorize. This easier approach consist in using <wif:FederatedPassiveSignIn> which is commonly used in ASP.NET WebForms applications.

To be able to use the built-in control, you have to make sure that your existing login view uses ASPX engine as it’s probably impossible to use the WIF’s control in a razor view. But of course this is not an issue, since in MVC you can choose engines separately for each view.

So if your web.config looks like this

<authentication mode="Forms">
    <forms loginUrl="~/Login/LoginPage" />
</authentication>

and the LoginController

public class LoginController : Controller
{
    //
    // GET: /Login/
    public ActionResult LoginPage()
    {
        return View();
    }
 
}

then you replace the existing LoginPage.cshtml with LoginPage.aspx (an ASPX instead of Razor view) and put this into the view:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<%@ Register Assembly="Microsoft.IdentityModel, Version=3.5.0.0, 
             Culture=neutral, PublicKeyToken=31bf3856ad364e35"
    Namespace="Microsoft.IdentityModel.Web.Controls" TagPrefix="wif" %>
<%@ Import Namespace="VULCAN.eSzkola.AD.BusinessLogic.WIF" %>
<%@ Import Namespace="Vulcan.eSzkola.AD.Common" %>
 
<!DOCTYPE html>
 
<html>
<head runat="server">
    <title>LoginPage</title>
 
    <script runat="server">
        protected void Page_Load( object sender, EventArgs ev )
        {
            SignIn.Issuer = ConfigurationManager.AppSettings["wsFederationIssuerUri"];
            SignIn.Realm  = Url.Action( "LoginPage", "Login", null, Request.Url.Scheme ); 
 
            SignIn.SignInError +=
                ( s, e ) =>
                {
                    SignIn.AutoSignIn = false;
                    lblError.Text = e.Exception.Message;
 
                    // log the exception
                };
        }
    </script>
 
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <wif:FederatedPassiveSignIn ID="SignIn" runat="server" AutoSignIn="true" 
             RequireHttps="false" RememberMeSet="false" />
        <asp:Label ID="lblError" runat="server" ForeColor="Red" />
    </div>
    </form>
</body>
</html>

And yes, this is just as simple.

Note that I need to set the Issuer (the address of the STS) (I read it from app settings) and the Realm (the address STS returns to) (I set it to the LoginPage [current] action).

What happens now is when the Authorize filter redirects requests to the LoginPage action on the Login controller, the FederatedPassiveSignIn control automatically redirects (AutoSignIn=”true”) to the STS and then when the response comes back from the STS the control picks it up and creates the WIF cookie.

Remember of course that for this to work you have to enable the SessionAuthenticationModule in your web.config:

<system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules runAllManagedModulesForAllRequests="true"/>
    <modules>
        <add name="SessionAuthenticationModule" 
           type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, 
                 Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, 
                 PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
    </modules>
</system.webServer>

Another trick is to use a slightly modified version of request validation routine. Normally we validate SAML responses like this:

public class FederatedRequestValidator : RequestValidator
 {
     protected override bool 
        IsValidRequestString( HttpContext context, string value, 
            RequestValidationSource requestValidationSource, 
            string collectionKey, out int validationFailureIndex )
     {
         validationFailureIndex = 0;
 
         if ( requestValidationSource == RequestValidationSource.Form &&
             collectionKey.Equals( WSFederationConstants.Parameters.Result, StringComparison.Ordinal ) )
         {
             SignInResponseMessage message = 
                WSFederationMessage.CreateFromFormPost( context.Request ) as SignInResponseMessage;
 
             if ( message != null )
             {
                 return true;
             }
         }
 
         return base.IsValidRequestString( context, value, requestValidationSource, 
             collectionKey, out validationFailureIndex );
     }
 }

However, it doesn’t work in MVC causing the stack to overflow as MVC 3 seems to utilize a new, lazy method of input validation (frankly, I’ve read the article two or three times and still don’t think I understand the whole point). The modified validator must then make sure it refers to unvalidated values from the request:

public class FederatedMVCRequestValidator : RequestValidator
{
    protected override bool IsValidRequestString( HttpContext context, string value, 
        RequestValidationSource requestValidationSource, 
        string collectionKey, out int validationFailureIndex )
    {
        validationFailureIndex = 0;
 
        if ( requestValidationSource == RequestValidationSource.Form &&
            collectionKey.Equals( WSFederationConstants.Parameters.Result, StringComparison.Ordinal ) )
        {
            var unvalidatedFormValues = 
               System.Web.Helpers.Validation.Unvalidated( context.Request ).Form;
 
            SignInResponseMessage message = 
                WSFederationMessage.CreateFromNameValueCollection( 
                   WSFederationMessage.GetBaseUrl( context.Request.Url ), unvalidatedFormValues ) 
                      as SignInResponseMessage;
            if ( message != null )
            {
                return true;
            }
        }
 
        return base.IsValidRequestString( context, value, requestValidationSource, 
           collectionKey, out validationFailureIndex );
    }
}

This was not obvious and I have finally found a clue at Technet (it is interesting however that the Technet page suggests that the modified validator is presented somewhere in WIF SDK but I cannot confirm it is).

Anyway, replacing the request validator and using FederatedPassiveSignIn on my ASPX view seems to be enough to integrate WIF and MVC 3.

2 comments:

Unknown said...

Hi Wiktor,

I am trying out an FederatedPassiveSignIn but I am getting the following error.

ID1038: The AudienceRestrictionCondition was not valid because the specified Audience is not present in AudienceUris.

I have audienceURis token in the page however. I am not sure why this has shown up.

Wiktor Zychla said...

Email me directly, I will try to help