========= Updated 06/01/2020 ===========
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.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.
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;
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.
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;
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;
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);
sig.SigningKey = cert2.PrivateKey;
Reference reference = new Reference();
reference.Uri = String.Empty;
reference.Uri = "#" + referenceValue;
XmlDsigEnvelopedSignatureTransform env = new
XmlDsigEnvelopedSignatureTransform();
XmlDsigExcC14NTransform env2 = new XmlDsigExcC14NTransform();
reference.AddTransform(env);
reference.AddTransform(env2);
sig.AddReference(reference);
KeyInfo keyInfo = new KeyInfo();
KeyInfoX509Data keyData = new KeyInfoX509Data(cert2);
keyInfo.AddClause(keyData);
sig.KeyInfo = keyInfo;
sig.ComputeSignature();
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.
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.
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) {
RelayState.Value = "http://www.davidsp8.com";
Dictionary<string,> attrs = new Dictionary<string,>();
attrs.Add("Email", Session["Email"].ToString());
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);
this.frmSSO.Action = "http://www.davidsp8.com/SSO.asmx";
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