Showing posts with label XML. Show all posts
Showing posts with label XML. Show all posts

Thursday, May 26, 2011

TEC 2011

For those of you who were able to make it to my presentation at TEC 2011 (State side), I promised a blog entry going into some more technical detail on the Ensynch Accelerated SQL XMA (coming soon).  If you are interested in the slides I presented you can get them here.  Jeremy also had another suggestion that I will be trying out, so keep an eye out, I will let you know how it goes!

Friday, April 22, 2011

File Based Management Agents In MIIS/ILM/FIM

I had a recent need to really compare the capabilities of each of the file based Management Agents in FIM.  Can you name all five? Don't worry, I won't leave you hanging, they are:

  • Attribute-value pair text file
  • Delimited text file
  • Directory Services Markup Language (DSML) 2.0
  • Fixed-width text file
  • LDAP Data Interchange Format (LDIF)

Here are some of the things that they can and can't do (this is for you Joe) and just for kicks, I also added in the SQL MA. If you are using one of these file types in an Extensible Management Agent (XMA), the following still applies: 

 

Multi-valued Attributes

Attribute Level Updates 1

Multi-valued Level Attribute Updates 2

Attribute-value pair

YES

NO

NO

Delimited

YES 3

NO

NO

DSML

YES

NO 4

NO

Fixed-width

YES 3

NO

NO

LDIF

YES

YES

ON IMPORT ONLY 5

SQL MA

YES

YES

NO 6



Okay, now for the caveats (can’t get away without some of those):

  1. An Attribute Level Update implies that a delta import can contain only the attribute that has changed (along with the other required columns, like the type of change and the anchor)

    So, here’s what that might look like.  Suppose I have a user with the following attributes:
      ID: 12345
      Name: Sarah
      Status: Active
      Phone: 555-123-4567
                                         
    If Sarah’s phone number changes to 555-987-6543, I can simply tell FIM something like: 
      ID: 12345 
      Type Of Change: Update
      Phone: 555-987-6543

    This has the advantage of giving FIM less work to do to determine what has changed on the records being imported and greatly speeds up delta imports. 
     
  2. A Multi-valued Level Attribute Update supports adding and deleting specific values from a multi-valued attribute
     
    Let’s take another look at Sara’s record:
      ID: 12345 
      Name: Sarah 
      Status: Active 
      Phone: 555-123-4567
      Phone: 555-456-7890
                                          
    Now, Sarah has two Phone numbers, or a single attribute with multiple values. With multi-value level attribute update support, we can do things like add a new phone number to the list, delete a phone number from the list or update a phone number (in essence by doing an add of the new value and then a delete of the old one):
      ID: 12345 
      Type Of Change: Add
      Phone: 555-987-6543

    Without this support, the source system would be required to do a “replace” action and provide FIM with all of the current values at the time of import which FIM will use to override all the values that it has for that attribute.  So if we start with Sarah’s record as listed just above and add the phone number 555-987-6543 and remove the phone number 555-123-4567, we would have to pass:
      ID: 12345 
      Type Of Change: Replace
      Phone: 555-987-6543
      Phone: 555-4567-7890

    As with attribute level updates, multi-valued level attribute update can greatly reduce the amount of work that FIM needs to accomplish.  To illustrate, just imagine applying this scenario to attributes like member on an AD group that can have thousands of values.
     
  3. Using a multi-valued attribute in a delimited or fixed-width file requires the use of a header on the import file

    So for a comma delimited file this would look like: 
                                           ID, NAME, PHONE, PHONE, PHONE
                                           12345, Sarah, 555-123-4567, 555-987-6543, 555-456-7890

    This would import a record for Sarah with three attributes - ID, NAME and PHONE, the last of which will have three values. A fixed width file would work the same way.
     
  4. While the DSML specifications themselves can actually handle attribute level updates using the addRequest, delRequest and modifyRequest operations, FIM only implements the ability to import a SearchResultEntry element which must contain all of the attributes on the object

    Just a side note for those that might be curious, you can actually place the addRequest, delRequest and modifyRequest nodes in the DSML file.  FIM will be able to parse the file and it wont cause any errors, however these elements are completely ignored and aren’t processed by FIM.  I also tried sending a DSML delta to FIM with just the attribute that changed and a change type of “modify”, and I suppose not surprisingly, the object in the connector space was updated so that it only had the one attribute I specified,  all the other attributes originally on the object were removed. Had any of these attributes been defined as required, this update would have failed.
     
  5. While you can import an update to a specific value in a multi-valued attribute, if you were to export this same change to an LDIF file, it will come through as a replace operation containing all values now present on the attribute
     
  6. While the SQL MA does not support updates to a specific value on a multi-valued attribute out of the box, I hear rumor that some customizations can be done to make this happen

Thursday, July 30, 2009

.NET Google SSO Part 2 of 2

This is the second in a series of articles discussing the implementation of an SSO application for Google Apps. The first .NET Google SSO Part 1 of 2 began the discussion by laying out the steps needed to handle the initial Authentication Request made by Google. This final piece will complete the package by illustrating the generation of the Authentication Response.


Google will expect the Authentication Response to come back as a form posted to the AssertionConsumerServiceURL provided by Google in the inital request. The form needs to have at least two elements on it, a <textarea> named SAMLResponse and an <input> named RelayState. The RelayState element is the url to redirect the user to at Google once the response has been sent. This is usually the same as the RelayState that Google provided to you via the RelayState url parameter.

Building the SAMLResponse is a little bit more involved. The SAMLResponse will contain the xml body of the AuthnResponse, which has the following format:

<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response ID="RESPONSE_ID" IssueInstant="ISSUE_INSTANT" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
        xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
            <Reference URI="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                <DigestValue>DIGEST_VALUE</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>SIGNATURE_VALUE</SignatureValue>
        <KeyInfo>
            <KeyValue>
                <RSAKeyValue>
                    <Modulus>RSA_KEY_MODULUS</Modulus>
                    <Exponent>RSA_KEY_EXPONENT</Exponent>
                </RSAKeyValue>
            </KeyValue>
        </KeyInfo>
    </Signature>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </samlp:Status>
    <Assertion ID="ASSERTION_ID" IssueInstant="ISSUE_INSTANT" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
        <Issuer>https://www.opensaml.org/IDP</Issuer>
        <Subject>
            <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress">
                USERNAME_STRING
            </NameID>
            <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <SubjectConfirmationData Recipient="ACS_URL" />
            </SubjectConfirmation>
        </Subject>
        <Conditions NotBefore="NOT_BEFORE" NotOnOrAfter="NOT_ON_OR_AFTER">
        </Conditions>
        <AuthnStatement AuthnInstant="AUTHN_INSTANT">
            <AuthnContext>
                <AuthnContextClassRef>
                    urn:oasis:names:tc:SAML:2.0:ac:classes:Password
                </AuthnContextClassRef>
            </AuthnContext>
        </AuthnStatement>
    </Assertion>
</samlp:Response>

Where the items in red will need to be replaced with actual values before sending the response to Google. Here are some specs for those items:

Attribute

Description 

Examples 

ASSERTION_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 

AUTHN_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 

NOT_BEFORE 

This defines the beginning timeframe for which the authentication is valid. 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

NOT_ON_OR_AFTER 

This defines the ending timeframe for which the authentication is valid. 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

ACS_URL 

The URL to send the authentication result

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

USERNAME_STRING 

The username of the person validated 

jsmith
jon.smith 

DIGEST_VALUE 

The value of the digest of the Reference URI calculated using the algorithm listed in the DigestMethod

Qo9gKjwBgNtt6P07aIZmIPXP+uQ=

SIGNATURE_VALUE 

The value of the digest of the SignedInfo Element calculated using the algorithm listed in the SignatureMethod

55khyrdDp0SgPmlu5dH49CAfASW2psDLhgjaB+Yl06pfxLJWnIctb7pX0K3k/vhNU8sUMY6Ps582CS6+YUPJps45U0i
VDK/+PPZvFREOwDR54XSzXAPd7GZyk+jBdQZv6D/g5IgccIGiw3Zi9Qpfe64iewSFoRukvVUD0yGOfszA=

RSA_KEY_MODULUS 

For use in the RSA encryption algorithm

K88ntfSyp1JU9Nu1KJwk+KKOuunT9G1RLwJXm6WrDb2klVGXLNKcP72lu5AlyGgYoZXO2bY2d610LE7kGol3Hu
PKfOMLoKqO/m1etjBBhQnuc2aVL1vJjQiLV/KsCsgwocNIoGOF01ndlgJiazFUFf6IwFltNg/A1pz9I05kkj0=

RSA_KEY_EXPONENT 

For use in the RSA encryption algorithm

AQAB

Here are the basic steps we need to take to create the AuthnResponse:

  1. Authenticate the user
  2. Generate an xmlDocument object using the AuthnResponse Template
  3. Fill in the AuthnResponse parameters
  4. Create a new signedXml object based on the xmlDocument
  5. Submit an html form with the InnerXml of the signedXml object and the RelayState

If we continue with the project created using the first blog post, we currently have one item on our form, a label called lblError. You will need to add two text boxes, txtUser and txtPwd to allow the user to enter their credentials and a button, btnSubmit. Format the form to your liking, and then add some code to authenticate the user, this can be done against a directory, a database, etc. This example will authenticate a user against ADAM.

// Class level vars
string googleUserName = "";

///<summary>

/// Authenticate user and return the result
///</summary>
protected bool AuthenticateUser()
{
    // User parameters from the form

    bool authenticated = false;

    // User parameters from the form
    string user = Request.Params["txtUser"];
    string password = Request.Params["txtPwd"];

    // Variables
    SearchResult srPerson = null;
    DirectorySearcher dSearch = null;

    DirectoryEntry de = null;
    DirectoryEntry deAdmin = null;
    string dn = "";

    // Clear out our class level variable, googleUserName
    googleUserName = "";

    try
    {
        if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(password))
        {
            // All required params were not entered, throw an error

            // could also use some field validators to avoid this
            throw new Exception("Logon failure: unknown user name or bad password");
        }

        // Get the users dn based on the user name used the form, so
        // that we can use it later with our bind test
        dSearch = new DirectorySearcher();
        dSearch.SearchScope = SearchScope.Subtree;
        dSearch.PropertiesToLoad.Add("distinguishedName");
        dSearch.SearchRoot = new DirectoryEntry("LDAP://YourServer:389/C=YourRootPath", "AdamAdminUser", "AdamAdminPswd",
              AuthenticationTypes.Secure | AuthenticationTypes.ServerBind); 
        dSearch.Filter = "(userName=" + user + ")";
        srPerson = dSearch.FindOne();
        dn = srPerson == null ? null : srPerson.Properties["distinguishedName"][0].ToString();

                
        if (!string.IsNullOrEmpty(dn))
        {
            // Get the directory entry, using the users credentials to bind
            de = new DirectoryEntry("LDAP://YourServer:389/C=YourRootPath", dn, password,
                  AuthenticationTypes.ServerBind | AuthenticationTypes.FastBind);

                    
            // Get the native object to force authentication, an error will be throw here
            // if the wrong credentials were given, this will be captured below and the user will be notified

            object bind = de.NativeObject;
 
            // They have authenticated okay, we are now assuming that their google user name
            // is different than their ADAM login name, we need to get it for use later

            // we cant use the above object to get the data because we had to issue our connection
            // using the fast bind option, get a new handle on the object using
            // admin credentials and the secure auth type

            deAdmin = new DirectoryEntry("LDAP://YourServer:389/" + dn, "AdamAdminUser", "AdamAdminPswd",
                  AuthenticationTypes.ServerBind | AuthenticationTypes.Secure);

            deAdmin.RefreshCache(new string[] { "googleUserName" });
 
            // If googleUserName is an email address, we just want what comes before the @ symbol
            googleUserName = deAdmin.Properties.Contains("googleUserName") ?
                  deAdmin.Properties["googleUserName"][0].ToString() : "";

            googleUserName = googleUserName.IndexOf("@") > 0 ?
                  googleUserName.Substring(0, googleUserName.IndexOf("@")) : googleUserName;

        
            // We didn't get what we expected throw an error

            if (googleUserName == "")
            {
                throw new Exception("Logon failure: unable to retrieve your Google username");
            }
            else
            {
                authenticated = true;
            }                    

        }
        else
        {
            // We cant continue without a dn

            throw new Exception("Logon failure: unknown user name or bad password");
        }
 
    }
    catch(Exception ex)
    {
        // Let user know there was a problem

        lblError.Text = ex.Message;
    }
    finally
    {
        // Clean up
        if (dSearch!= null)

        {
            dSearch.Dispose();
        }

        if (de != null)

        {
            de.Dispose();
        }

        if (deAdmin != null)
        {
            deAdmin.Dispose();
        }
    }
}

Now that our user is authenticated and we have their Google UserName, the next step is to generate an xmlDocument object using the AuthnResponse template. The template that we need is similar to the AuthnResponse shown above, with one very important difference; we don't want the <Signature> element or its children. We will be adding this using the signedXml object later. Create an xml file named AuthnResponseTemplate in your project directory. Copy in the AuthnResponse xml below and save the document.

<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response ID="RESPONSE_ID" IssueInstant="ISSUE_INSTANT" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
        xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </samlp:Status>
    <Assertion ID="ASSERTION_ID" IssueInstant="ISSUE_INSTANT" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
        <Issuer>https://www.opensaml.org/IDP</Issuer>
        <Subject>
            <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress">
                USERNAME_STRING
            </NameID>
            <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <SubjectConfirmationData Recipient="ACS_URL" />
            </SubjectConfirmation>
        </Subject>
        <Conditions NotBefore="NOT_BEFORE" NotOnOrAfter="NOT_ON_OR_AFTER">
        </Conditions>
        <AuthnStatement AuthnInstant="AUTHN_INSTANT">
            <AuthnContext>
                <AuthnContextClassRef>
                    urn:oasis:names:tc:SAML:2.0:ac:classes:Password
                </AuthnContextClassRef>
            </AuthnContext>
        </AuthnStatement>
    </Assertion>
</samlp:Response>

We will now add code to consume this new file and replace the elements in red with proper values:

///<summary>
/// Create our xml SAML Response base on the template
///</summary>
private string
CreateSamlResponse()

{
    // Grab our response template

    string filepath = Request.PhysicalApplicationPath + ("AuthnResponseTemplate.xml");
    XmlDocument doc = new XmlDocument();
    doc.PreserveWhitespace = true;
    doc.Load(filepath);
    string samlResponse = doc.InnerXml; 

    // Get our IDs and make sure that they arent the same (timing)

    string responseID = CreateID();
    string assertionID = CreateID();
 
    while (responseID == assertionID)
    {
        assertionID = CreateID();
    } 

    // Fill out out our template

    samlResponse = samlResponse.Replace("USERNAME_STRING", googleUserName);
    samlResponse = samlResponse.Replace("RESPONSE_ID", responseID);
    samlResponse = samlResponse.Replace("ISSUE_INSTANT", GetDateAndTime(0, 0));
    samlResponse = samlResponse.Replace("AUTHN_INSTANT", GetDateAndTime(0, 1));
    samlResponse = samlResponse.Replace("NOT_BEFORE", GetRequestAttributes(requestXmlString, "IssueInstant"));
    samlResponse = samlResponse.Replace("NOT_ON_OR_AFTER", GetDateAndTime(1, 0););
    samlResponse = samlResponse.Replace("ASSERTION_ID", assertionID);
    samlResponse = samlResponse.Replace("ACS_URL", GetRequestAttributes(requestXmlString, "AssertionConsumerServiceURL"));
            
    return samlResponse; 
}

///<summary>

/// Generate a random set of alpha characters
/// appropriate for our SAML IDs

///</summary>
public static
string CreateID()

{
    Random random = new Random();
    char[] charMapping = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'};
    byte[] bytes = new byte[20]; // 160 bits
    random.NextBytes(bytes);
 
    char[] chars = new char[40];
 
    for (int i = 0; i < bytes.Length; i++)
    {
        int left = (bytes[i] >> 4) & 0x0f;
        int right = bytes[i] & 0x0f;
        chars[i * 2] = charMapping[left];
        chars[i * 2 + 1] = charMapping[right];
    }
 
    string id = new string(chars);
 
    return id;
}

///<summary>

/// Create a datetime string in the appropriate SAML format
///</summary>
public static
string GetDateAndTime(int days, int minutes)

{
    DateTime dt = DateTime.Now;
 
    if (days > 0)
    {
        dt = dt.AddDays(days);
    }
 
    if (minutes > 0)
    {
        dt = dt.AddMinutes(minutes);
    }
 
    return dt.ToString("yyyy-MM-dd") + 'T' + dt.ToString("HH:mm:ss") + 'Z';
}

///<summary>

/// Get the specified attribute value from the SAMLRequest
///</summary>
private string
GetRequestAttributes(string xmlString, string attribute)

{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(xmlString);
 
    if (doc != null)
    {
        return doc.DocumentElement.GetAttribute(attribute).ToString();                
    }
    else
    {
        throw new Exception("Error parsing AuthnRequest XML: Null document");
    }
}

Next, we will be adding the xml signature. Be sure to update the file path to the x509 cert used for signing the xml. This certificate must contain the private key:

///<summary>
/// Sign the AuthnResponse xml adding the Signature element
///</summary>
private string
SignResponse(string xmlDocString)

{
    // Get our pfx cert w/private and public keys
    // we have the document on the file system, but you could also

    // get it from the certificate store
    // this is the same cert (with public keys only) that was provided
    // to Google during the SSO set-up
    X509Certificate2 x509 = new X509Certificate2();
    x509.Import("C:\FilePathToGoogleCert\Cert.pfx");
 
    // Create a new RSA signing key
    RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider();
    rsaKey = (RSACryptoServiceProvider) x509.PrivateKey; 

    // Load our xml into a doc

    XmlDocument doc = new XmlDocument();
    doc.PreserveWhitespace = true;
    doc.LoadXml(xmlDocString);
 
    // Sign the XML document.
    // Create a SignedXml object.
    SignedXml signedXml = new SignedXml(doc);
 
    // Add the key to the SignedXml document.
    signedXml.SigningKey = rsaKey;
            
    // Add KeyInfo
    KeyInfo keyInfo = new KeyInfo();
    keyInfo.AddClause(new RSAKeyValue(rsaKey));
    signedXml.KeyInfo = keyInfo;
                        
    // Create a reference to be signed.
    Reference reference = new Reference();
    reference.Uri = "";
 
    // Add an enveloped transformation to the reference.
    XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
    reference.AddTransform(env);
 
    // Add the reference to the SignedXml object.
    signedXml.AddReference(reference);
 
    // Compute the signature.
    signedXml.ComputeSignature();
 
    // Get the XML representation of the signature and save
    // it to an XmlElement object.
    XmlElement xmlDigitalSignature = signedXml.GetXml();
 
    // Append the element to the XML document.
    doc.DocumentElement.InsertBefore(doc.ImportNode(xmlDigitalSignature, true), doc.DocumentElement.FirstChild);
 
    return doc.InnerXml.Trim();
}

Finally, we will bring it all together in the submit button on_click event handler and generate the html form with the SAMLResponse and RelayState and send it on to Google:

///<summary>
/// Handle the submit button click event, authenticate the user,
/// then create the response and submit it to Google

///</summary>
protected void
btnSubmit_Click(object sender, System.EventArgs e)

{
    // Authenticate user
    if (AuthenticateUser())
    {

        // Get our SAMLResponse and sign it
        string samlResponse = CreateSamlResponse();

        string signedResponse = SignResponse(samlResponse);
 
        string formName = "authnResponse";
        string acsUrl = GetRequestAttributes(requestXmlString, "AssertionConsumerServiceURL");

        // Write the http response in such a way that the html document
        // will submit a form with our data on it to the requestor
 
        System.Web.HttpContext.Current.Response.Clear();
        System.Web.HttpContext.Current.Response.Write("<html><head/>");
        System.Web.HttpContext.Current.Response.Write(string.Format("<body onload='document.{0}.submit()'>",
              formName));

        System.Web.HttpContext.Current.Response.Write(string.Format("<form name='{0}' method='post' action='{1}'>",
              formName, acsUrl));

        System.Web.HttpContext.Current.Response.Write(string.Format("<textarea name=\"{0}\" style='visibility:hidden'>{1}</textarea>",
              "SAMLResponse", signedResponse));

        System.Web.HttpContext.Current.Response.Write(string.Format("<input name=\"{0}\" type=\"hidden\" value=\"{1}\">",
              "RelayState", Request.Params["RelayState"]));

        System.Web.HttpContext.Current.Response.Write("</form>");
        System.Web.HttpContext.Current.Response.Write("</body></html>");
        System.Web.HttpContext.Current.Response.End(); 
    }
}

That should do it! The user will then be allowed into their Google account.

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.