Monday, November 16, 2009

The Missing ILM Extension

While recently browsing the Microsoft.Metadirectory services namespace for ILM I discovered a new extension that I had not seen before. In addition to the 6 that I was familiar with:
  • IMAExtensibleCallExport
  • IMAExtensibleFileExport
  • IMAExtensibleFileImport
  • IMAPasswordManagement
  • IMASynchronization
  • IMVSynchronization

There was a 7th that I had never hear of, the IMACalloutExtension. Curious, I began to investigate. The Extension implements the following methods:

public void BeginExportToCd(string connectTo, string user, string domain, string password)
{
}

public void EndExportToCd()
{
}

public void BeforeExportEntryToCd(string deltaEntryXml, string[] changedAttributes)
{
}

public void AfterExportEntryToCd(byte[] origAnchor, string origDN, string origDeltaEntryXml, byte[] newAnchor, string newDN, string failedDeltaEntryXml, string errorMessage)
{
}

The only documentation that I could find was within the interface definition itself. It appears that this extension was to be implemented using a new Management Agent of type "Callout" (or something similar) and gives the developer the ability to take action both before and after each entry export action, which could be extremely helpful. I haven't been able to find a roadmap for this feature, but it appears to be something we can look forward to in the future!

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.

Thursday, June 18, 2009

Invalid provider type specified

I recently had the need to encrypt some items in an application configuration file to secure some account passwords that were being stored there. After consulting with Brad Turner, we decided to use an x509 certificate to provide the public and private keys needed for RSA encryption. He requested one from our local certificate authority for this purpose and placed it in the machine store. I was able to easily retrieve the cert and encrypt the data, however, I ran into some issues attempting to use it for decryption. While trying to do an explicit conversion from the x509Certificate2.PrivateKey property to an RSACryptoServiceProvider object:

    RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)x509.PrivateKey;

I received an "Invalid provider type specified" error. I was unable to find anything related to this specific problem online, however, we were able to surmize that the error was refering to the cryptographic provider type used to create the certificate. Based on some previous experience, Brad knew that the new v3 template for certificates on Windows 2008 server can cause some issues for older technologies. After creating a new certificate using an older template (v2), this error was no longer an issue. This may be fixed with the 4.0 Framework, but be aware, if you are using 3.5 or older, you may run into this problem.

Sunday, April 19, 2009

Cannot connect to MMS WMI Service

Having trouble with the WMI service for MIIS/ILM? Try running the following commands at a command prompt:

  1. regsvr32 "C:\Program Files\Microsoft Identity Integration Server\Bin\mmswmi.dll"
  2. mofcomp -N:root\MicrosoftIdentityIntegrationServer "C:\Program Files\Microsoft Identity Integration Server\Bin\mmswmi.mof"

This will re-register the libraries with the OS. Give the WMI call another try, if you continue to have problems with an Access Denied error…

  1. Open the Component Services Console in the Administrative Tools Folder.
  2. Navigate to Console Root, Computers, My Computer, DCOM Config and then Microsoft Metadirectory Services.
  3. Right click on the application entry and select Properties.
  4. Go to the security tab and alter any security necessary.
  5. Click OK on the Application Properties page and close out the Component Services and Computer Management Consoles.

Sunday, April 5, 2009

A Small MIIStake

Okay, so to be fair the issue is actually with ILM (Version 3.3.118.0). In the previous version, MIIS 3.2.559.0, I would frequently run MAs from the Operations tab, like so... From the Operations tab, right click on the MA run of interest, and select the Run… option:


The resulting Run Management Agent window would then pre-populate the Management agent drop-down box and the Run profile list box with the same values as the original operation selected. Simply click OK and the selected run would be restarted (which makes it really easy to re-run a particular profile):


Now if you attempt to do follow this same procedure in ILM…


The drop down menu is not automatically set to the MA selected. If you are used to being able to simply click the OK button from this dialog, you may inadvertently run the wrong MA. In fact you can see where I ran an ADAM MA by accident in the middle of the test MA series of runs:


So the bad news, for those of you using the current version of ILM, get use to using the drop-down menu to select the MA you want to run. The good news… this is a planned fix in a future hotfix! Not sure which one, or when. Anyone out there know?

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.

Sunday, January 25, 2009

Anyone know where I can find a good Locksmith?


I recently had the need to develop a WebPart for a client that needed to be able to "unlock" a site collection. For any of you that have tried this from within a WebPart running on the SharePoint platform, you know there are quite a few issues that can come up.

There are four different types of locks that can be set, here is a quick mapping of how those locks match up with the SPSite locking properties:

Not locked / nonesite.writeLocked = false
site.readOnly = false
site.readLocked = false
Adding content prevented / noadditionssite.writeLocked = true
site.readOnly = false
site.readLocked = false
Read-only / readonlysite.writeLocked = false
site.readOnly = true
site.readLocked = false
No access / noaccesssite.writeLocked = false
site.readOnly = false
site.readLocked = true

Additionally, when a site is locked using any one of the lock options, the site.LockIssue is used to describe the reason for the lock. If the site was locked using the Central Administraion console, the Additional lock information (LockIssue) field is required when locking the site. However, this parameter is not set when locking a site using the command line interface stsadm.

Here are just a few of the error messages I ran into while trying to set the lock options outright in my WebPart:
  1. Updates are currently disallowed on GET requests. To allow updates on a GET, set the 'AllowUnsafeUpdates' property on SPWeb.
  2. The security validation for this page is invalid. Click Back in your Web browser, refresh the page, and try your operation again.
  3. Access to this Web site has been blocked.
  4. Attempted to perform an unauthorized operation.
  5. Access denied.
With the exception of the first error, the remainder of the messages all center around permissions and access. With that in mind, I went about trying to build some solutions using the typical SharePoint methods to elevate the privileges of the code being executed. The first of these is the SPSecurity.RunWithElevatedPrivileges. With impersonation turned on in our web.config file, the code is running under the context of the SharePoint user. This method appears to elevate the code to run under the service account on the application pool.
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite site = new SPSite(siteUrl))
{
site.AllowUnsafeUpdates = true;
site.ReadLocked = false;
site.ReadOnly = false;
site.WriteLocked = false;
site.LockIssue = "";
}
});
Next, with no more luck, I tried to initialize a instance of the site class running under the context of my administration account.
using (SPSite site = new SPSite(siteUrl, SPContext.Current.Web.AllUsers["domain\\adminUsername"].UserToken))
{
site.AllowUnsafeUpdates = true;
site.ReadLocked = false;
site.ReadOnly = false;
site.WriteLocked = false;
site.LockIssue = "";
}
After a little bit more investigating, I discovered that in order to perform the unlock procedures that I needed, full blown code impersonation was needed. With a little bit of additional code, I had the access that I needed.
//get global admin access to perform the work
ImpersonateValidUser("domain", "adminUsername", "password");

using (SPSite site = new SPSite(siteUrl))
{
site.WebApplication.FormDigestSettings.Enabled = false;
site.AllowUnsafeUpdates = true;
site.ReadLocked = false;
site.ReadOnly = false;
site.WriteLocked = false;
site.LockIssue = "";
}

UndoImpersonation();
The code to implement the ImpersonateValidUser and UndoImpersonation functions can be found here: http://www.codeproject.com/KB/cs/zetaimpersonator.aspx, a wonderful bit of code from Uwe Keim.

By adding the line site.WebApplication.FormDigestSettings.Enabled = false to the code, we are able circumvent error #2. All of the remaining errors should be taken care of by our code impersonation. However, be warned that if the site has either readOnly or readLocked set to true, you will still have issues accessing values of various site properties. You will need to unlock the site to get the values that you need. If desired, you can then set the locks back to their previous state (assuming you have saved the state somewhere). And yes, you will even run into errors simply trying to save off this state for later use. The specific type of error that gets thrown will tell you everything you need to know about which of the three locks has been set.

When implementing the code, be sure that it is not executing when the page initially loads, otherwise you will run into error #1. Rather run the code during a page post back (i.e. in an event handler to manage the WebPart form submittal).