Wednesday, August 31, 2011

The quest for customizing ADFS sign-in web pages, part 3 – an example RP application

Just the quick summary – we are working on a custom STS which the ADFS will federate with. In the previous blog entry we’ve coded a custom STS and the aim of this post is to write an example RP application federated with the STS.

Let’s start with a basic structure, an ASP.NET application consisting of two pages, LoginPage.aspx and Default.aspx. Configure <authentication> to Forms and create a proper <authorization> entry with usual “deny users=? and allow users=*” respectively.

Probably you are interested why do we configure <authentication> as Forms. Why this is so if we want the RP to be federated with the STS so the Forms authentication should have nothing to do here.

The answer is – we will use one of the two possible methods of federation which is the FederatedPassiveSignIn web control. The control has to be put on a web page and the page should be the first one to present to users. The control will be responsible for initiating the WS-Federation dialogue between the application and the STS and for consuming the response from the STS. Forms authentication is then great as it will automatically redirect all unauthenticated requests to the login page. From there, the FederatedPassiveSignIn control will handle the rest of the process.

The control itself is rather straightforward, you just have to configure it. The content of the RP’s LoginPage is then as follows:

<body>
    <form id="form1" runat="server">
    <div>
      <wif:FederatedPassiveSignIn ID="WSFederationLogin"             
            runat="server" 
            DisplayRememberMe="false" 
            RequireHttps="false" 
            Realm="http://localhost:40001/LoginPage.aspx"
            Issuer="http://localhost:40000/Default.aspx"
            VisibleWhenSignedIn="false">
        </wif:FederatedPassiveSignIn>    
    </div>
    </form>
</body>

Note the two attributes, Realm which points to the location of the LoginPage containing the control (the RP application we are currently implementing) and the Issuer which points to the custom STS we’ve implemented in the previous blog entry. When the page is rendered, the control looks like a button users can click to redirect to the STS and the nice thing is that you can either customize the look-and-feel of the button or even set it’s AutoSignIn attribute to “true” to force the automatic redirect to the STS (the login page containing the control will then never be visible to users).

When the control consumes the response from the STS, another module has to be responsible for maintaining the session and it’s the SessionAuthenticationModule. You can think that it replaces FormsAuthenticationModule as the authentication provider for your RP application – it uses the claim set from the federation cookie to build the ClaimsIdentity object at every single request to your application (just like FormsAuthenticationModule does using the Forms cookie). Note that FormsAuthenticationModule is still active, as we’ve used it to redirect all unauthenticated requests to the LoginPage and fortunately these two, Forms and Session authentication modules work together well. The RP’s web.config has to look like this then:

<configuration>
 
    <configSections>
        <section name="microsoft.identityModel" 
                 type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    </configSections>
 
    <appSettings>
        <add key="wsFederationIssuerName" value="The.Custom.STS"/>
        <add key="wsFederationThumbprint" value=""/>
    </appSettings>
    
    <system.web>
 
        <authentication mode="Forms">
            <forms loginUrl="LoginPage.aspx" defaultUrl="Default.aspx" name="The.Custom.RP.Cookie" />
        </authentication>
        <authorization>
            <deny users="?"/>
            <allow users="*"/>
        </authorization>
        
        <compilation debug="true" targetFramework="4.0" />
 
        <httpModules>
            <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        </httpModules>
 
        <httpRuntime requestValidationType="The.Custom.RP.FederatedRequestValidator" />
 
    </system.web>
 
    <microsoft.identityModel>
        <service>
            <audienceUris>
                <add value="http://localhost:40001/LoginPage.aspx"/>
            </audienceUris>
            <federatedAuthentication>
                <cookieHandler name="The.Custom.RP.FederationCookie" requireSsl="false"/>
            </federatedAuthentication>
            <issuerNameRegistry type="The.Custom.RP.FederatedIssuerNameRegistry"/>
        </service>
    </microsoft.identityModel>
    
    <system.webServer>
        <validation validateIntegratedModeConfiguration="false"/>
        <modules>
            <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        </modules>
    </system.webServer>
 
    <system.diagnostics>
        <sources>
            <source name="Microsoft.IdentityModel" switchValue="Verbose">
                <listeners>
                    <add name="xml" type="System.Diagnostics.XmlWriterTraceListener" initializeData="The.Custom.RP.e2e"/>
                </listeners>
            </source>
        </sources>
        <trace autoflush="true"/>
    </system.diagnostics>
 
</configuration>

Things to note here:

  1. we inform ASP.NET that we will use a custom configuration section
  2. we add http modules in two places, system.web/httpModules and system.webServer/modules – this is a common trick so that modules work correctly in both the integrated web server and IIS
  3. we have the microsoft.identityModel section where we provide:
    1. the audienceUri – this is the adress (or a list of addresses) which are valid response endpoints for messages routed from the STS to the RP. Because the only valid endpoint in our RP application to consume STS response is the login page (containing the FederatedPassiveSignIn control), we have a single Uri here. For your information – the audienceUri can be configured programmatically.
    2. the name of the Federated cookie which is responsible for maintaining the session. For your information - the cookie is usually too big to be held in a single http cookie so if your STS provides many claims you will notice that there are in fact few http cookies created at the RP’s side
  4. a custom issuerNameRegistry – we’ll comment this later on
  5. a custom requestValidationType – we’ll comment this later on
  6. a system.diagnostics section which enables WIF debugging. If something’s wrong during the login process, you can take a look into the “The.Custom.RP.e2e” file and find a detailed error message there. It’s really handy!

The two above custom providers, the issuerNameRegistry and the requestValidationType serve following purposes.

First, the issuerNameRegistry is used at the RP’s side to check what exact certificate has been used to sign in incoming claims. Suppose you do not care and do not validate it. Then, a simple attack on your RP application is possible involving a custom STS which creates claims and returns to your RP. If you only care about claims and not the signing certificate, you will not be able to tell the difference between valid claims (issued by a trusted STS) and invalid claims (issued by any STS). Most examples use a built-in ConfigurationBasedIssuedNameRegistry which uses the data from the microsoft.identityModel section of the web.config, however a custom issuerNameRegistry can be more flexible.

public class FederatedIssuerNameRegistry : IssuerNameRegistry
{
    public override string GetIssuerName( 
        System.IdentityModel.Tokens.SecurityToken securityToken )
    {
        X509SecurityToken x509Token = securityToken as X509SecurityToken;
        if ( x509Token != null &&
             x509Token.Certificate != null
            )
        {
            if ( string.IsNullOrEmpty( WsFederationThumbprint ) ||
                 x509Token.Certificate.Thumbprint == WsFederationThumbprint
                )
                return WsFederationIssuerName;
        }
 
        throw new SecurityTokenException( "Untrusted issuer." );
    }
 
    public string WsFederationThumbprint
    {
        get
        {
            return ConfigurationManager.AppSettings["wsFederationThumbprint"];
        }
    }
 
    public string WsFederationIssuerName
    {
        get
        {
            return ConfigurationManager.AppSettings["wsFederationIssuerName"];
        }
    }
}

Note what’s going on. When WIF is up to validate the incoming token, the GetIssuerName method will be invoked with the incoming token. In my implementation I look into the token’s certificate and if it’s present I am trying to compare the certificate’s thumbprint with the one I stored in my appSettings. However, if there’s no thumbprint in my appSettings, I am unconditionally accepting the certificate. This is great for debugging!

Second, the custom request validator is used at the RP’s side to validate the incoming response from the STS. The problem with this STS response is that it contains the SAML token in the request body and because ASP.NET is rather paranoid about such requests, the standard request validator will reject such requests as possible attacks on your application! Before ASP.NET 4 the only option was to turn off the validation, however in 4.0 you provide a custom request validator:

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 );
    }
}

Note that the validator tries to interpret the incoming request as the WIF’s SignInResponseMessage and if it succeedes, the request is accepted. All other requests are pushed back to the standard request validator.

The last element of our example RP application is the Default.aspx page which we use to show the list of incoming claims. Just put a GridView there and in the codebehind add:

GridViewClaims.DataSource =
      ( this.User.Identity as IClaimsIdentity )
          .Claims
          .Select( c =>
              new
              {
                  ClaimType = c.ClaimType,
                  ClaimValue = c.Value,
                  ClaimValueType = c.ValueType.Substring( c.ValueType.IndexOf( "#" ) + 1 ),
                  ClaimSubject = 
                        c.Subject != null && 
                        !string.IsNullOrEmpty( c.Subject.Name ) ? c.Subject.Name : 
                        "[none]",
                  ClaimIssuer = c.Issuer ?? "[none]"
              }
              );
GridViewClaims.DataBind();

(taken out of one of WIF’s SDK examples)

We are almost there. The remaining problem concerns the certificate. You see, since the certificate our STS uses to sign tokens is self-created (with Portecle), it’s chain cannot be validated. However the default WIF’s policy will reject such certificate. What we have to do is to swich the policy to the less restricted one. Just add the global application class to the RP application and inform the pipeline that you’d like to accept all certificates and disable the validation of the chain:

protected void Application_Start( object sender, EventArgs e )
{
    System.Net.ServicePointManager.ServerCertificateValidationCallback = 
        ( a, b, c, d ) => true;
 
    FederatedAuthentication.ServiceConfigurationCreated +=
         ( s, fede ) =>
         {
             fede.ServiceConfiguration.CertificateValidationMode = 
                 System.ServiceModel.Security.X509CertificateValidationMode.None;
         };
}

This is it. Your RP application is ready. Remember that the actual address of the STS is put in the Issuer attribute of the FederatedPassiveSignIn control put on the LoginPage. You can easily swich to other STSes just by changing this address and/or providing a valid thumbprint of the certificate in appSettings.

In our next entry we will federate the ADFS with our STS and our RP with the ADFS so that we’ll have a chain:

RP – R-STS (ADFS) – IP-STS (custom STS)