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.

7 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.

The Twilight Troll said...

I setup ADFS on our test site and within that under Relying party trust I setup my application's claim rules to send back an e-mail address under the "Issuance Transform Rules" tab.

Using your code, I am able to direct the user to ADFS where they have to type in their Active Directory User/Pass which when successful sends them back to the page where federatedAuthModule.IsSignInResponse(request) is true.

However, when I read through the principal.Claims, not one of the claims (there are 2) contain the authenticated e-mail address. Any ideas?

The Twilight Troll said...

I have a few questions about this code. 1) I see a section for x509 cert. Do I need to programmatically give a thumbprint somewhere to validate by? 2) What are the characteristics of the cookie and session? What I'm interested in is, I put this in my page_load, through my adfs server I login and select a link to take me to this page which authenticates me. When I view the browser code I do not see a cookie. When I close out of the browser and go in I need to log-in again. Shouldn't the browser retain the cookie and continue the log-in process automatically? Thank You

Charles Barnett said...

Hi! Thanks for the help. I have emailed you for guidance. I am getting the an error on the following line:

fam.FederationConfiguration = FederatedAuthentication.FederationConfiguration

The error is:

The value of the property 'type' cannot be parsed. The error is: The type 'myWebApp.Web.AuthorizationManager,myWebApp.Web' cannot be resolved. Please verify the spelling is correct or that the full type name is provided.

I am totally new to SAML and ADFS so excuse me if this is something that should be obvious to me. If you could help me, though, that would be great.

Charles Barnett said...

I have followed your example and it is now implemented on our system. I have a question that might make me look like a noob. The network engineer is saying that he needs two endpoints from me, one for login and one for logout. I am new to both saml and federated authentication. How do I find the endpoint for the HTTP module? Any advice on creating the logout endpoint or places to go on the web to help me with this task? I am currently developing in VS 2015 on a server running IIS 10. Thanks

Peter Murdoch said...

Thanks for posting! I've found details around this topic to be tricky as the landscape continues to evolve. So far federating the whole site with config and a library that overrides wsfederationmodule has been pretty effective for us, but with multi-tenanting it will be interesting to see whether that approach will hold. It is certainly handy to be able to make any iis virtual directory sso aware with a only a web.config.