Monday, November 24, 2014

Simplest SAML1.1 Federated Authentication

Yet another entry on Federated Authentication. This time we focus on the Relying Party side and our goal is to federate with an STS with as little effort as possible and as much code as possible, no unnecessary configuration sections in web.config. The STS could be any SAML1.1 compliant STS: the ADFS2, the Azure Federation Service, Thinktecture Identity Server.

Last time I’ve blogged on the SessionAuthenticationModule and how it is used to create and then retain a session of authenticated user. The WS-FAM (or FAM; WS-Federated Authentication Module) will be used to redirect the flow to the STS and read the response.

Fortunately, the FAM is also integrated in .NET 4.5. I won’t need it in my web.config, however, the SAM won’t work if there is nothing in the configuration file. Make sure to refer to the previous blog entry to get all the details about configuring SAM in web.config.

Assuming you have your login resource (a login page), an example code to integrate with an STS would be as follows. The implementation has two conditional branches:

  • one for the very first GET of the login resource – this is where we build a WS-Federation compliant request and go to the STS for authentication
  • one for the STS response – this is where we get the SAML token out of the response and validate it for signature validity and the certificate acceptance
public partial class LoginPage : System.Web.UI.Page
{
    // a web forms example but MVC version would be the same
    protected void Page_Load( object sender, EventArgs e )
    {
        // sam is configured in web.config
        var sam = FederatedAuthentication.SessionAuthenticationModule;
 
        // fam is not
        var fam                     = new WSFederationAuthenticationModule();
        fam.FederationConfiguration = FederatedAuthentication.FederationConfiguration;
 
        var request = new HttpContextWrapper( this.Context ).Request;
 
        // is this the response from the STS
        if ( !fam.IsSignInResponse( request ) )
        {
            // no
 
            // the STS
            fam.Issuer = "https://your.sts.address/here";
            // the return address
            fam.Realm  = this.Request.Url.AbsoluteUri;
 
            var req      = fam.CreateSignInRequest( string.Empty, null, false );
 
            // go to STS
            Response.Redirect( req.WriteQueryString() );
        }
        else
        {
            // yes
 
            // get the SAML token
            var securityToken = fam.GetSecurityToken( request );
 
            var config = new SecurityTokenHandlerConfiguration
            {
                CertificateValidator = X509CertificateValidator.None,
                IssuerNameRegistry   = new CustomIssuerNameRegistry()
            };
            config.AudienceRestriction.AudienceMode = AudienceUriMode.Never;
 
            var tokenHandler = new SamlSecurityTokenHandler
            {
                CertificateValidator = X509CertificateValidator.None,
                Configuration        = config
            };
 
            // validate the token and get the ClaimsIdentity out of it
            var identity  = tokenHandler.ValidateToken( securityToken );
 
            var principal = new ClaimsPrincipal( identity );
 
            var token = sam.CreateSessionSecurityToken( principal, string.Empty,
                  DateTime.Now.ToUniversalTime(), DateTime.Now.AddMinutes( 20 ).ToUniversalTime(), false );
 
            sam.WriteSessionTokenToCookie( token );
 
            this.Response.Redirect( this.Request.QueryString["ReturnUrl"] );
        }
    }
 
 
    // a simple helper to get the absolute uri of a local uri
    private Uri GetAbsoluteUri( string redirectUrl )
    {
        var redirectUri = new Uri( redirectUrl, UriKind.RelativeOrAbsolute );
 
        if ( !redirectUri.IsAbsoluteUri )
        {
            redirectUri = 
               new Uri( new Uri( Request.Url.GetLeftPart( UriPartial.Authority ) + Request.ApplicationPath ), redirectUri );
        }
 
        return redirectUri;
    }
}

We also need a custom issuer registry (referenced in the code above) to make sure we accept the certificate. An example registry below accepts any certificate, make sure you customize it to look into a concrete registry:

public class CustomIssuerNameRegistry : IssuerNameRegistry
{
    public override string GetIssuerName( SecurityToken securityToken )
    {
        X509SecurityToken x509Token = securityToken as X509SecurityToken;
 
        return x509Token.Certificate.Subject;
    }
}

I really like the simplcity of the code and the fact it is so concise. As I always do, I strongly encourage to read the handbook on Claims-Based Identity and Access Control.

2 comments:

AK said...

@Wiktor, Thanks for the blog. It was really helpful. One more thing I wanted to check with you was the certification/toke validation process. In your blog you have given a sample implementation of "IssuerNameRegistry" interface. Can you please elaborate what should be included in the concrete implementation? I am thinking of checking following conditions:
1. Name of the issuer
2. Intended audience
3. Condition: Not before and Not after
4. Certificate it self

Does the “SamlSecurityTokenHandler” class checks this internally or do we need to check this explicitly?

Wiktor Zychla said...

@AK: it depends on your requirements. In an example multitenant app, I pass the current tenant name to the issuer name registry and validate the certificate's subject name and thumbprint with a white list of certificates allowed for this tenant.