DEV Community

Cover image for Working example of SAML Single Sign-On integration using C#
Anton Yarkov
Anton Yarkov

Posted on

Working example of SAML Single Sign-On integration using C#

What is Single Sign-On at all?

Suppose you have a web application that people are using to do one thing X, but you are doing it great. For example, it would be a web store allowing to order a custom T-shirt printing by uploading some funny and pretty images found in the internet.

You are looking for ways to extend its functionality by adding some more capabilities for your users, but you don't want to lose the focus on this thing that you are doing best of all.

One way would be to integrate your web application with other services, such as a cool image provider, a trends meter, a delivery service, etc. It would allow your users to seamlessly move between different services (for example: looking for a cool image, checking trends, and uploading this image to your custom printing service) and do multiple things using only your web application.

Single Sign-On simplifies authentication flow and allows your users to sign in only once (on your web application) with their login/password (or even more innovative mechanisms, like FIDO2, if you use such) and use more integrated services by simple clicks without the need to log in again and again. It allows you to extend the functionality of your product by adding more value with relatively low investments.

SAML

SAML is one of the standard ways of doing Single Sign-On. For a long time, extensive enterprise services use this mechanism as one of the most secure and proven methods to exchange sensitive authentication and authorization information, like logins, passwords, emails, user accounts, etc. But it's not that complicated to add this solution between smaller businesses and enable cool integrations.

SAML is one of the most secure way to integrate with third-party among many other options. It allows parties to use asymmetric encryption (RSA) based on secure X.509 certificates. As of 2021, the standard is in version 2.

I should note that it might be better for you to trust one of many existing boxed solutions with a proven history and from vendors responsible for their code and overall solution. However, as a developer, I know that businesses may need much more flexibility and customization in their products, so I want to provide more details and working example using C#, so you can easily reuse this with your ASP.NET application.

SAML Workflow

Usually, the "classic" SAML workflow includes 3 parties:

  • Service Provider - this is a third-party service you want to integrate with
  • Identity Provider - this is some (enterprise) trused authentication service, that is able to proof the user identify and tell the Service Provider that "he is OK!".
  • User Agent - a browser with your Web Store opened by a user

This "classic" scenario is suitable for the enterprise, and we are not going to stop for long here. You can see the workflow diagram below and it is explained in all details on Wikipedia.

Classic SAML

However, we are a small business, right? So, usually, we don't have complicated agreements with Enterprise-level Identity Providers, but we still need to integrate with third-party. Let's see how it should look like for us.

First step of the integration

The first thing is an agreement with some service provider to have an integration with your service. This time we focus on SAML protocol specifically, but I know about tenish more custom integration algorithms used by many small and medium-sized services. They use custom HTTP API, REST, WCF, XML, and JSON-based data formats, JWTokens and OAuth, and sometimes even combine all things. Whatever option is used, they need to provide you with some configuration parameters to establish an initial test connection.

For SAML-based integrations, they will ask you to provide so-called "metadata information." That's a small XML file that contains information about your service. This file is being used by service providers representatives to automatically create a certification (aka test) endpoint that would allow you to test the connection, resolve all the roadblocks and certify everything before you go public.

The critical part here is the XML content of this file that must be signed with your private X.509 certificate (aka private key, in terms of RSA). Of course, you have to buy one. Some providers allow using self-signed credentials for testing purposes. That's a matter of your agreement with the service provider.

I've prepared some code that is open source and publicly available on my GitHub account. This small console application allows you to generate a Metadata.xml file and properly sign it with the private key.

Now, you should send your Public X.509 Certificate (as a *.crt file, or PEM text) and this Metadata.xml via secure channel.

As soon as service provider replies with some connection endpoints, the first part might be over.

Integration workflow

You have a web application that implements some authentication workflow and allows users to sign in with their login and password. It means that you are the Identity Provider, and by doing this, you can eliminate some redundant steps and make a process simpler. So, imagine that we combine Identity Provider and your Web Store into one entity, like this:

Simple SAML scenario

It simplifies the overall picture. However, it's not that simple under the hood. What actually should happen in the end is we should have an XML document called SAMLResponse prepared and encoded in Base64. You have to send this document via an HTTP POST request to the service provider to authenticate the user and automatically redirect his browser to the third-party target service — see detailed steps in the diagram below.

Under the hood of SAML authentication

Again, I've prepared a set of utility classes written in C#, so you can easily see how to execute those steps in your web application.

Below, you can find my comments on every important aspect of that process. But I believe, it will be much more easy to follow the steps in class SamlIntegrationSteps.cs -> method BuildEncodedSamlResponse(...).

Step 1. Creating SAML assertion XML is not harder than create a usual XML document. But you have to follow the SAML specification and documentation provided by your Service Provider to use the appropriate field names. Working example is here SamlAssertionAlgorithms.cs.

AssertionType assertion = new AssertionType
{
    ID = ...Assertion Id ...,
    IssueInstant = ...UTC Time...,
    Version = "2.0",
    Issuer = new NameIDType
    {
        Value = "...your Web Store URL here..."
    },
    Subject = new SubjectType
    {
        Items = new object[]
        {
            new NameIDType
            {
                Format = "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
                Value = userData.GetUserEmail()
            },
            new SubjectConfirmationType
            {
                Method = "urn:oasis:names:tc:SAML:2.0:cm:bearer",
                SubjectConfirmationData = new SubjectConfirmationDataType
                {
                    NotOnOrAfter = ...UTC Time....AddMinutes(3),
                    NotOnOrAfterSpecified = true,
                    Recipient = settings.Recipient
                }
            }
        }
    },
    Conditions = new ConditionsType
    {
        NotBefore = ...UTC Time...,
        NotBeforeSpecified = true,
        NotOnOrAfter = ...UTC Time....AddMinutes(3),
        NotOnOrAfterSpecified = true,
        Items = ...conditions that you need...
    },
    Items = new StatementAbstractType[]
    {
        new AttributeStatementType
        {
            // ReSharper disable once CoVariantArrayConversion
            Items = ...attributes array...
        },
        new AuthnStatementType
        {
            AuthnInstant = ...UTC Time...,
            SessionIndex = ...Assertion Id ...,
            AuthnContext = new AuthnContextType
            {
                ItemsElementName = new [] { ItemsChoiceType5.AuthnContextClassRef },
                Items = new object[] { "urn:federation:authentication:windows" }
            }
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Step 2. Signing of the SAML assertion can be done as described in official Microsoft docs. Working example is here SamlAssertionAlgorithms.cs.

Step 3. Encryption of the SAML assertion is implemented in SamlAssertionAlgorithms.cs using System.Security.Cryptography.Xml.

X509Certificate2 x509 = ...get certificate...

xmlElement.SigningKey = x509.PrivateKey;
xmlElement.SignedInfo.CanonicalizationMethod = SamlSignedXml.XmlDsigExcC14NTransformUrl;

// Create a reference to be signed. 
Reference reference = new Reference
{
    Uri = "#" + referenceValue
};

reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.AddTransform(new XmlDsigExcC14NTransform());

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

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

xmlElement.KeyInfo = keyInfo;

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

// Put the sign as the first child of main Request tag.
xmlAssertion?.InsertAfter(xmlElement, xmlAssertion.ChildNodes[0]);
Enter fullscreen mode Exit fullscreen mode

Step 4. Adding the assertion document to the SAMLResponse is just about adding XML:

XmlDocument encryptedAssertion = new XmlDocument();

// Add namespaces
XmlDeclaration xmlDeclaration = encryptedAssertion.CreateXmlDeclaration("1.0", "UTF-8", null);
XmlElement encryptedRoot = encryptedAssertion.DocumentElement;
encryptedAssertion.InsertBefore(xmlDeclaration, encryptedRoot);

// Form Assertion element
XmlElement encryptedAssertionElement = encryptedAssertion.CreateElement("saml",
    "EncryptedAssertion", "urn:oasis:names:tc:SAML:2.0:assertion");
encryptedAssertion.AppendChild(encryptedAssertionElement);

// Add encrypted content
var encryptedDataNode = encryptedAssertion.ImportNode(encryptedData.GetXml(), true);
encryptedAssertionElement.AppendChild(encryptedDataNode);

// Form a document
var root = xmlDocument.DocumentElement;
var node = root.OwnerDocument.ImportNode(encryptedAssertionElement, true);
root.RemoveChild(xmlAssertionSource ?? throw new InvalidOperationException());
root.AppendChild(node);
Enter fullscreen mode Exit fullscreen mode

Step 5. Signing the SAMLResponse is about almost the same as siging the Assertion. Working example is here SamlResponseAlgorithms.cs.

The End

Some service providers may avoid specific steps and do not require the signature or encryption. In this case, you may remove these parts of the code. Overall, feel free to use my examples in your apps, and I hope this will help you and your application engage users and grow the customer base.

Cheers!

Related links

Examples of SAML responses
Tips&Tricks on documenting custom API.

Discussion (0)