Friday, March 27, 2009

.NET Google SSO Part 1 of 2

Creating an SSO for Google can be a bit of a challenge. There are quite a few good examples for doing this in Java, but not as many for .NET. Here is my attempt to resolve that issue. This article will be broken up into two parts, the first of these is handling the authentication request from Google, the second of these will illustrate the response generation.

Let's start by getting a good overview of the design. The entire process will begin when the end-user tries to access their Google Account. If the Google Apps service has been configured to use SSO, Google will generate an Authentication Request and send it on for you to handle in your SSO. It is then up to you to perform user authentication and generate a signed Authentication Response to send back to Google.



 

Okay, now the details. When you receive an AuthenticationRequest from Google, it will make an http request to the URL you have specified in your Google Apps account with two additional query string parameters: SAMLRequest and RelayState. The SAMLRequest will look like a series of random letters and numbers that is in actuality a base64 encoded string containing the AuthnRequest. RelayState is the RFC 1783 encoded URL that the user is ultimately trying to get to, in this case probably their Google email account. So this would look something like:

http://www.yourCompany.com/pathToPage/yourSSOPage.aspx?SAMLRequest=eJxdkN1uwjAMRl8lyn1%2f6NCEIgpim7YhsQlB2cXuQuK2KW2cxSni8Sk%2fk6bd2v7s4zOdn7qWHcGTQZvzUZxyBlahNrbK%2ba54jSZ8PpuS7FonFn2o7QZ%2beqDAhpwlcW3kvPdWoCRDwsoOSAQltouPlcjiVDiPARW2nC1fcl6rpqnLWtkDqIPU7tA4BftS1pVrGq1LpVF1pQbOvn6hsgvUkqiHpaUgbRhKaTqJRmmUTYrRo3jIxHj8zdn6funJ2Bv%2fP6z4L9b%2bNkTivSjW0Qa08aDCdcnRaPCfQyLnFWLVQqyw42xBBD4MSM9oqe%2fAb8EfjYLdZjX8FYITSdKikm2NFJK3a7IYVCUXb3dtsSR34iyZnQGwVYL1&RelayState=http%3a%2f%2fmail.google.com%2fa%2fyourCompany.com

Before we go much further, let's talk about the AuthnRequest. The AuthnRequest is an XML document that has the following format:

<?xml version="1.0" encoding="UTF-8" ?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="AUTHN_ID"
    Version="2.0"
    IssueInstant="ISSUE_INSTANT"
    ProtocolBinding="urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect"
    ProviderName="PROVIDER_NAME"
    AssertionConsumerServiceURL="ACS_URL" />

Where the items in red will be replace with actual values by Google at the time of generation. Here are some specs for those items:

Attribute

Description 

Examples 

AUTHN_ID

A 160-bit string made up of randomly generated lower case alpha characters from a through p

dfpccklacbmnmioodokleambcplpmfghahihdmna

ISSUE_INSTANT

Should be the current date and time in the format:
yyyy-mm-ddThh:mm:ssZ

2000-01-01T23:01:01Z
2008-10-02T05:55:23Z 

PROVIDER_NAME

This is the domain name of the calling application

google.com 

ACS_URL

The URL to send the authentication result to

https://www.google.com/a/yourCompany.com/acs

Your first task will be determining the values of the two query parameters mentioned above and decoding them. In order to perform some of the decoding you will need to use a compression utility, I use a nice open source one available from ic#code at http://www.icsharpcode.net/OpenSource/SharpZipLib/.

So let's begin! Start a new web project and then make sure you add the following references: ICSharpCode.SharpZipLib and System.Security. Edit the default.aspx page, adding the following using statements…

using System;
using System.Collections;
using System.ComponentModel;

using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Text;
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.DirectoryServices;
using System.Security.Permissions;
using System.Security.Cryptography.X509Certificates;

Next, are the basic steps we need to take to decode the AuthnRequest:

  1. Retrieve data from the query string
  2. Decode the base64 string to a byte array
  3. Decompress (inflate) the array
  4. UTF8 Encode the array back into a string

So here's what the code looks like to do all of that, however, you will first need to add a label to your form called lblError to communicate any issues back to the user:

///<summary>
/// Converts the SAMLRequest query string parameter back to
/// its native, user readable XML format
///</summary>
private string decodeAuthnRequestXML()
{
    try
    {
        // Get the SAMLRequest parameter
        string encodedRequestXmlString = Request.Params["SAMLRequest"];
 
        if (encodedRequestXmlString != null && encodedRequestXmlString != "")
        {
            // Base64 decode it
            byte[] base64DecodedByteArray = Convert.FromBase64String(encodedRequestXmlString);

            // Uncompress the AuthnRequest data
            // First attempt to unzip the byte array according
            // to DEFLATE (rfc 1951)
            try
            {   

                Inflater inflater = new Inflater(true);
                inflater.SetInput(base64DecodedByteArray);
                       
                // since we are decompressing, we dont
                // know how much space we might need
                // hopefully this number is suitably big
                byte[] xmlMessageBytes = new byte[2048];
                int resultLength = inflater.Inflate(xmlMessageBytes);
 
                if (!inflater.IsFinished)
                {
                    lblError.Text = "Error decoding AuthnRequest: " +
                        "Decompression data overflow";
                    return null;
                }
 
                // UTF8 encode the result and return it
                UTF8Encoding utf8 = new UTF8Encoding();
                return utf8.GetString(xmlMessageBytes, 0, resultLength);
 
            }
            catch (Exception e)
            {
 
                // if DEFLATE fails, then attempt to unzip
                // the byte array according to zlib (rfc 1950)
                MemoryStream msIn = new MemoryStream(base64DecodedByteArray);
                MemoryStream msOut = new MemoryStream();
                InflaterInputStream iis = new InflaterInputStream(msIn);
                byte[] buf = new byte[2048];
                int count = iis.Read(buf, 0, buf.Length);
                while (count != 0)
                {
                    msOut.Write(buf, 0, count);
                    count = iis.Read(buf, 0, buf.Length);
                }
                    
                iis.Close();
                byte[] retVal = new byte[msOut.Length];
                msOut.Position = 0;
                msOut.Read(retVal, 0, (int) msOut.Length);    

                // UTF8 encode the result and return it
                return utf8.GetString(retVal);

            }
        }
        else
        {
                lblError.Text = "Error decoding AuthnRequest: " +

                        "Unable to read SAMLRequest";
                return null;
        }

    }
    catch (Exception e)
    {
        throw new Exception("Error decoding AuthnRequest: " +
                    "Check decoding scheme - " + e.Message);
    }
}

This code can be called from the Page_Load method to validate the request before prompting the user for their credentials. Be sure to check the Page.IsPostBack property to only call decodeAuthnRequestXML function on the initial page load.

// Class level vars
string requestXmlString = null;

///<summary>
/// Handles the page onload event
/// Begins Authentication Request decoding
///</summary>
private void Page_Load()
{
    if (!IsPostBack)
    {
        // Validate initially to force asterisks
        // to appear before the first roundtrip.
        requestXmlString = decodeAuthnRequestXML();
    }
}

The string you get back from the decodeAuthnRequestXML function should be user readable XML with the AuthnRequest format mentioned above. It should look something like:

<?xml version="1.0" encoding="UTF-8" ?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="hcjjhfhcnkeckadpkjpcebfahgpjjddfcdocmfde"
    Version="2.0"
    IssueInstant="2009-03-26T16:32:44Z"
    ProtocolBinding="urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect"
    ProviderName="google.com"
    AssertionConsumerServiceURL="https://www.google.com/a/yourcompany.com/acs" />

Once you have that, you can parse the XMLDocument and begin building your response. Look for Part 2 of the SSO series to demonstrate the building of the AuthnResponse.