Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Performing a SAML Post with C#

0.00/5 (No votes)
7 Mar 2010 16  
Code performing a SAML Post which can be used for Single Sign On applications

========= Updated 06/01/2020 ===========

Image 1

Introduction

SAML (Security Assertion Markup Language) is an XML and protocol standard used mostly in federated identity situations. For the most part, you will see SAML used with Single Sign On implementations. Basically, it is a standard way of passing authentication information securely across domain boundaries.

One of the common ways to pass SAML parameters is via an HTTP POST parameter. The .NET framework has all of the components necessary to accomplish this. There are also packages and components that you can purchase that literally does the same thing. However, knowing several key points will allow you to do this yourself.

Background

Several terms will be used throughout this article which are used in many SAML Single Sign On implementations.

  • Identity Provider: This is the site or organization where the user's identity originates from. Typically in a SSO scenario, wherever the user enters in his/her credentials is the Identity Provider.
  • Service Provider: This is the site or organization where the user is Single Signing On to.
  • Browser/Post method: This method uses a simple HTTP POST to pass the payload. The payload is simply a POST parameter which is a base64 encoded XML string. This XML sting is protected by being signed with an X509 Certificate utilizing the WS-Security standard.
  • Browser/Artifact profile method: This method initiates a session that notifies the target to call back to get the payload.

SAML has two main ways to communicate. One way is utiltizing the Browser/Post profile and the other uses a Browser/Artifact profile. This article will outline the simpler, less secure Browser/Post method. For more information on SAML: SAML Wiki. For more information on signing XML: W3C XML Signature Standards site.

Overview of the Code

In order to accomplish a SAML post, the overall process will take place by accomplishing three major steps:

  • An XML transaction will built based on the XML standard.
  • The XML will need to be signed.
  • And finally, the entire string will be Base64 encoded.

Building the XML based on the standard

This step is actually the easiest and also the most tedious task. The .NET Framework comes with a tool which can read XSD Schemas and generate .NET classes. These classes can then be serialized into XML. Note that there are actually three schemas being imported. The first schema is used for signing the document, and the SAML standard has two schemas: one used for protocols, and the other for assertions. These XSDs were downloaded from the each of the particular standards website.

// Create the schema classes using the .Net command line
// utility, pass in the three schemas which will be used
xsd.exe xmldsig-core-schema.xsd xenc-core-schema.xsd 
    saml-schema-assertion-2.0.xsd  saml-schema-protocol-2.0.xsd 
    /classes /namespace:davidsp8.common.Security.Saml20 /outputdir:..

Once the classes are created, the task of building the XML can be started. In SAML terms, when you build a message, you build two main XML nodes. The main node is the Response node, and the other is the Assertion node. The following is an example of what a SAML Response tag might look like:

<Response xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 

     xmlns:xsd="http://www.w3.org/2001/XMLSchema" 

     ID="_d4809d44-9f6b-46cd-bcd1-9fe5dce33cd8" 

     Version="2.0" IssueInstant="2010-02-05T02:25:09.765625Z" 

     Destination="http://www.davidsp8.com/SSO.asmx" 

     xmlns="urn:oasis:names:tc:SAML:2.0:protocol">
  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">davidsp8.com:idp</Issuer>
  <Status>
    <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
  </Status>
  ....
</Response>

Creating the above node is as simple as building some classes that were created by running the XSD utility. Note that all DateTime values are passing UTC DateTime values.

ResponseType response = new ResponseType();
// Response Main Area
response.ID = "_" + Guid.NewGuid().ToString();
response.Destination = recipient;
response.Version= "2.0";
response.IssueInstant = System.DateTime.UtcNow;

NameIDType issuerForResponse = new NameIDType();
issuerForResponse.Value = issuer.Trim();
        
response.Issuer = issuerForResponse;

StatusType status = new StatusType();

status.StatusCode = new StatusCodeType();
status.StatusCode.Value = 
  "urn:oasis:names:tc:SAML:2.0:status:Success";

response.Status = status;

Now, we'll need to create an Assertion. Technically, any SAML Response can have more than one Assertion. However in our example, we'll only be creating one. The Assertion typically contains data necessary to authenticate the user. It also contains Conditions that dictate how long the Assertion is valid. Here's an example of what an Assertion might look like.

<Assertion Version="2.0" 

        ID="_18d9be83-4e26-4c47-825a-5df08b0ebdf8" 

        IssueInstant="2010-02-05T02:25:10.75Z" 

        xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
    <Issuer>davidsp8.com:idp</Issuer>
    <Subject>
      <NameID NameQualifier="davidsp8.com">test</NameID>
      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData 

          NotOnOrAfter="2010-02-05T02:30:10.7812500Z" 

          Recipient="http://www.davidsp8.com/SSO.asmx" />
      </SubjectConfirmation>
    </Subject>
    <Conditions NotBefore="2010-02-05T02:25:10.75Z" 

        NotOnOrAfter="2010-02-05T02:30:10.75Z">
      <AudienceRestriction>
        <Audience>davidsp8.com</Audience>
      </AudienceRestriction>
    </Conditions>
    <AuthnStatement AuthnInstant="2010-02-05T02:25:10.75Z">
      <AuthnContext>
        <AuthnContextClassRef>AuthnContextClassRef</AuthnContextClassRef>
      </AuthnContext>
    </AuthnStatement>
    <AttributeStatement>
      <Attribute Name="email" 

            NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <AttributeValue xsi:type="xsd:string">a@bcdef.com</AttributeValue>
      </Attribute>
    </AttributeStatement>
  </Assertion>

Again, we'll use the classes we created using the XSD.exe utility. Conditions are added, most notably the NotBefore and the NotOnOrAfter variables. Together, these variables are used to validate the Assertion.

// Here we create some SAML assertion with ID and Issuer name. 
AssertionType assertion = new AssertionType();
assertion.ID = "_" + Guid.NewGuid().ToString();

NameIDType issuerForAssertion = new NameIDType();
issuerForAssertion.Value = issuer.Trim();

assertion.Issuer = issuerForAssertion;
assertion.Version = "2.0";

assertion.IssueInstant = System.DateTime.UtcNow;

//Not before, not after conditions 
ConditionsType conditions = new ConditionsType();
conditions.NotBefore = DateTime.UtcNow;
conditions.NotBeforeSpecified = true;
conditions.NotOnOrAfter = DateTime.UtcNow.AddMinutes(5);
conditions.NotOnOrAfterSpecified = true;

AudienceRestrictionType audienceRestriction = 
                        new AudienceRestrictionType();
audienceRestriction.Audience = new string[] { domain.Trim() };

conditions.Items = new ConditionAbstractType[] {audienceRestriction};

The Subject is built. The SAML Subject is typically some kind of unique identifier used by the identity provider. It's usually used to tie back to a particular user. For example, if an SSO is occurring from Company A to Company B, often, the Subject would contain Company A's user ID. This would provide a tie back to the user performing the SSO. More on SAML Subject can be found in the SAML Standard.

//Name Identifier to be used in Saml Subject
NameIDType nameIdentifier = new NameIDType();
nameIdentifier.NameQualifier = domain.Trim();
nameIdentifier.Value = subject.Trim();

SubjectConfirmationType subjectConfirmation = 
                        new SubjectConfirmationType();
SubjectConfirmationDataType subjectConfirmationData = 
                            new SubjectConfirmationDataType();
        
subjectConfirmation.Method= "urn:oasis:names:tc:SAML:2.0:cm:bearer";
subjectConfirmation.SubjectConfirmationData = subjectConfirmationData;
// 
// Create some SAML subject. 
SubjectType samlSubject = new SubjectType();

AttributeStatementType attrStatement = new AttributeStatementType();
AuthnStatementType authStatement = new AuthnStatementType();
authStatement.AuthnInstant = DateTime.UtcNow;
AuthnContextType context = new AuthnContextType();
context.ItemsElementName = 
  new ItemsChoiceType5[] { ItemsChoiceType5.AuthnContextClassRef };
context.Items = new object[] { "AuthnContextClassRef" };
authStatement.AuthnContext = context;
                    
samlSubject.Items = new object[] { nameIdentifier, subjectConfirmation };

assertion.Subject = samlSubject;

The Subject locality identifies where the transaction originated from.

IPHostEntry ipEntry =
    Dns.GetHostEntry(System.Environment.MachineName);

SubjectLocalityType subjectLocality = new SubjectLocalityType();
subjectLocality.Address  = ipEntry.AddressList[0].ToString();

Each SAML Assertion typically has attributes that are sent to the Service Provider to assist in identifying the user. Typically, these attributes are a combination of attributes that uniquely identify the user.

attrStatement.Items = new AttributeType[attributes.Count];
int i = 0;
// Create userName SAML attributes. 
foreach (KeyValuePair<string,> attribute in attributes) {
    AttributeType attr = new AttributeType();
    attr.Name = attribute.Key;
    attr.NameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic";
    attr.AttributeValue = new object[] { attribute.Value };
    attrStatement.Items[i] = attr;
    i++;
}</string,>

Finally, we finish the assertion.

assertion.Conditions = conditions;
assertion.Items = 
  new StatementAbstractType[] { authStatement, attrStatement };
return assertion;

Siging the XML

Now that we've built a Response and an Assertion, we have a complete message which is ready to be sent. Before sending, we need to sign the XML with a certificate. This certificate would be one that we hold the private key to, and the Service Provider that we are sending this post to will need to verify the validity of our message using this same certificate. The SAML Standard also allows for signing the assertion. For this particular example, we are only signing the Response.

public static XmlElement SignDoc(XmlDocument doc, X509Certificate2 cert2, string referenceId, string referenceValue) {
    SamlSignedXml sig = new SamlSignedXml(doc, referenceId);
    // Add the key to the SignedXml xmlDocument. 
    sig.SigningKey = cert2.PrivateKey;

    // Create a reference to be signed. 
    Reference reference = new Reference();

    reference.Uri = String.Empty;
    reference.Uri = "#" + referenceValue;

    // Add an enveloped transformation to the reference. 
    XmlDsigEnvelopedSignatureTransform env = new
        XmlDsigEnvelopedSignatureTransform();

    XmlDsigExcC14NTransform env2 = new XmlDsigExcC14NTransform();
            
    reference.AddTransform(env);
    reference.AddTransform(env2);

    // Add the reference to the SignedXml object. 
    sig.AddReference(reference);

    // Add an RSAKeyValue KeyInfo (optional; helps recipient find key to validate). 
    KeyInfo keyInfo = new KeyInfo();
    KeyInfoX509Data keyData = new KeyInfoX509Data(cert2);

    keyInfo.AddClause(keyData);
            
    sig.KeyInfo = keyInfo;

    // Compute the signature. 
    sig.ComputeSignature();

    // Get the XML representation of the signature and save it to an XmlElement object. 
    XmlElement xmlDigitalSignature = sig.GetXml();

    return xmlDigitalSignature;
}

You will notice in the above code that we are using our own implementation of the SignedXml object provided by the framework. This is because without this code, ComputeSignature will fail for our ID reference.

The following code will ensure that while the document is getting signed, the correct reference is created.

public class SamlSignedXml : SignedXml {
    private string _referenceAttributeId = "";
    public SamlSignedXml(XmlDocument document, 
                 string referenceAttributeId) : base(document) {
        _referenceAttributeId = referenceAttributeId;
    }
    public override XmlElement GetIdElement(
        XmlDocument document, string idValue) {
        return (XmlElement)
            document.SelectSingleNode(
                string.Format("//*[@{0}='{1}']", 
                _referenceAttributeId, idValue));
    }
}

At this point, either the Response or the Assertion is signed. Based on the SAML standard, either method is acceptable.

XmlElement signature =
    SigningHelper.SignDoc(doc, cert, "ID", 
    signatureType == SigningHelper.SignatureType.Response ? response.ID : assertionType.ID);

In the end, the entire code is exposed via the following SamlHelper. It's worth mentioning that the entire code so far has been SAML version 2.0. The code which can be downloaded with this article can also build a SAML 1.1 Response.

/// <summary>
/// GetPostSamlResponse - Returns a Base64 Encoded String with the SamlResponse in it.
/// </summary>
/// <param name="recipient">Recipient</param>
/// <param name="issuer">Issuer</param>
/// <param name="domain">Domain</param>
/// <param name="subject">Subject</param>
/// <param name="storeLocation">Certificate Store Location</param>
/// <param name="storeName">Certificate Store Name</param>
/// <param name="findType">Certificate Find Type</param>
/// <param name="certLocation">Certificate Location</param>
/// <param name="findValue">Certificate Find Value</param>
/// <param name="certFile">Certificate File 
/// (used instead of the above Certificate Parameters)</param>
/// <param name="certPassword">Certificate Password 
/// (used instead of the above Certificate Parameters)</param>
/// <param name="attributes">A list of attributes to pass</param>
/// <param name="signatureType">Whether to sign Response or Assertion</param>
/// <returns>A base64Encoded string with a SAML response.</returns>
public static string GetPostSamlResponse(string recipient, 
	string issuer, string domain, string subject,
	StoreLocation storeLocation, StoreName storeName, X509FindType findType, 
	string certFile, string certPassword, object findValue,
    	Dictionary<string, string> attributes, 
	SigningHelper.SignatureType signatureType)

Running the Demo Application

The demo application is a WinForms application. I've done this because I need to often issue a post from a given server. So making a WinForms application makes it easy to move around and test.

Image 2

In order to run the demo application, you need to be able to SSO into a site that is SAML aware. 

  • Version: The demo application can generate both version 1.1 and 2.0 of SAML. 
  • Issuer: A unique ID of the person/organization that is issuing the SAML request. For demonstration purposes, this can be any unique value. Typically, it's an ID of the Identity Provider.
  • Recipient: This should be the endpoint that is expecting the SAML post. 
  • Target: Typically, this is the target you'd like the user directed to.
  • Domain: This is the domain of the Service Provider. The Service Provider may require that this be a certain value.
  • Subject: The subject is often used to tie a user back to the user account from the Identity Provider. Often, it's the user ID of the user on the Identity Provider.
  • Signature Certificate: The certificate can be any certificate that you hold the private key for. This is an important part of the overall security of SAML. In the demo application's case, what will happen is that the certificate will be sent along with the request. The request's signature will be validated, then the user will set up an account. During account setup, the certificate will be saved with the user account, and any subsequent requests will be validated against this certificate. Generally, this certificate is not transmitted this way. There usually is a manual exchange between the entities.
  • Encryption Certificate: The certificate can be any certificate that you have the public key for. The Service Provider must have the private key to decrypt.
  • Attributes: Will be the attributes required by the Service Provider.

To create a self signed certificate, use PowerShell command "New-SelfSignedCertificate":

New-SelfSignedCertificate -CertStoreLocation Cert:\CurrentUser\My 
     -subject <your name here> -KeyExportPolicy Exportable 
     -NotAfter (Get-Date).AddYears(1) -Type CodeSigningCert -KeySpec Signature

In the demo application, select the "Select Certificate" button and enter the following values: Store Location=CurrentUser, Store Name=Root, FindMethod=FindBySubjectName, FindValue=<your name here>.

The demo application makes use of the WebBrowser control so that the entire demo can be ran from the WinForms application. The first time you access the site, you will be prompted to register a user.

In a typical SSO scenario, this user creation either happens by transferring the common user account to the service provider via some kind of data feed, or some sort of "private" information known to the user is requested to verify their identity. Both of these methods adds a layer of security on top of the bare SSO solution.

Using the Code

Using the code in an ASP.NET application is a little harder to execute than outlined here. This is because the framework does not really have an easy way to perform an HTTP POST programmatically. There are several ways to accomplish this. I usually use the JavaScript submit method.

To use the code, develop your application as you typically would. Develop a page which will perform the SSO and place two asp:input controls on the page. For SAML 2.0, the control names should be SAMLResponse and RelayState. Place a div tag around the input fields, and set the style to "display:none" so that it will not show to the user.

<body runat="server" id="bodySSO"

    <form id="frmSSO" runat="server" enableviewstate="False">
    <center><img src="http://www.codeproject.com/Images/Progress.gif" 

       alt="Redirecting to external site..." /></center>
    <center><asp:Label ID="lblMessage" runat="server" 

           Text="Redirecting to external site..." 

           EnableViewState="False"></asp:Label>
        <br />
    </center>
    <div style="display:none" >
        <input id="SAMLResponse" type="text" 

           runat="server" enableviewstate="False"/>
        <input id="RelayState" type="text" 

           runat="server" enableviewstate="False"/>
    </div>
    </form>
</body>

Then, in the Page_Load event, set the appropriate fields, and call the helper function to get the SamlResponse variable.

protected void Page_Load(object sender, EventArgs e) {
    // Set Relay State
    RelayState.Value = "http://www.davidsp8.com";
    // Set Attrs
    Dictionary<string,> attrs = new Dictionary<string,>();
    attrs.Add("Email", Session["Email"].ToString());
    // Set SAML Response
    SAMLResponse.Value =
        SamlHelper.GetPostSamlResponse(
        "http://www.davidsp8.com/SSO.asmx", 
        "davidsp8.com:sp", 
        "davidsp8.com", 
        "localuserid",
        StoreLocation.LocalMachine, StoreName.Root, 
          X509FindType.FindByThumbprint, null, null,
          "41fe9204effd0d8c5e65a1de3a507da1383fd14f", attrs);

    //Set Form Action
    this.frmSSO.Action = "http://www.davidsp8.com/SSO.asmx";
            
    // Add OnLoad Logic so that form will be submitted.
    HtmlGenericControl body = 
      (HtmlGenericControl)this.Page.FindControl("bodySSO");
    if (body != null) {
        body.Attributes.Add("onload", 
           "document.forms.frmSSO.submit();");
    }
}

Points of Interest

When deciding on how to do this, I took several approaches which did not all work. The first approach I took was to use classes that exist in the framework. The .NET Framework version 3.5 has an Assertion class. This class will serialize a SAML assertion. However, there does not seem to be a Response class in the framework so the response would need to be added manually. Also, the Assertion class required that the assertion be signed, but the SAML standard does not require this.

There is much more to SAML. For example, in version 2.0, the attributes can be encrypted.

History

  • 9th February, 2010: Initial post
  • 7th March, 2010: Article updated
  • 1st June, 2020: Article udpated

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here