Implementing IdP Initiated SAML SSO and RESTful API

Follow

Applies to: Pro, Plus, & Enterprise Plans

Introduction

This article details an example of implementing Single Sign-On (SSO) from a development perspective, using both Absorb's Single Sign-on and RESTful API features. For the purposes of this article we'll assume a basic understanding of the SSO process along with some background in development. A higher level explanation of IdP initiated SSO can be found here. More information on our RESTful API can be found here & here.

Once a user is authenticated in your system, the Identity Provider (IdP), the user will look to navigate to your Absorb Training portal, the Service Provider (SP). Most likely this will be done through a link that passes them to Absorb. To do this, your system should first confirm the user exists in your Absorb portal. If the user does not exist, it should be created then logged in. It’s possible to automate this process using the REST API. Below I'll go over a simple implementation of this that will check if the authenticating user exists and if not, it will be created using the API.

C# sample code from this article can be downloaded at the very bottom of the page.

Verifying the User

Before we get into the code you should have an API key provided to you through support or your CSM. This will be needed to correctly authenticate. Along with that you will need the credentials of an admin account with the appropriate permissions to view and create users. This account should be able to manage all users so it can correctly find any user attempting to login. Lastly this example will make use of a package called RestSharp (http://restsharp.org/) to build the rest client and make requests.

public static bool VerifyUser(string user)
{
    //Generate authentication token
    var token = GetToken(REST_USER, REST_USER_PASSWORD, API_KEY);

    //Check if the user exists
    var doesUserExist = LookUpUser(token, user);

    if (!doesUserExist)
    {
        //Default department to place users in.
        var defaultDepartment = Guid.Parse("c0b49894-4674-4faf-ba6a-be86b4f7078e");
        doesUserExist = CreateUser(token, user, defaultDepartment);
    }

    return doesUserExist;
}

This method is designed to take in a user's username and return true based on if the account exists or if it doesn't, it will return true if a user was successfully created during the process. If none exists and the account was not created successfully it will return false and login will not proceed. It’s expected that this method will be called before initiating SSO.

We'll start by generating a token using the hard-coded values for the Rest Admin and my supplied API key. The most common error with this is related to the credentials being wrong. You should be able to login with that account to ensure the username and password are correct. We have initialized the rest client with the other class variables which should take in the base URL of your portal.

public static RestClient RestClient = new RestClient("https://company.myabsorb.com/api/Rest/v1");

Generating the token can be done in the following way,

private static string GetToken(string username, string restPass, string restPrivateKey)
{
	var request = new RestRequest("Authenticate", Method.POST);
	var authenticateModel = new {Username = username, Password = restPass, PrivateKey = restPrivateKey};

	request.AddJsonBody(authenticateModel);
	var response = RestClient.Execute(request);

	return response.Content.Trim('"');
}

The request is essentially "POST https://company.myabsorb.com/api/Rest/v1/authenticate" with the authentication model attached to the body. If the credentials are all correct this should return the trimmed token.

With the token stored we can now search for the user attempting to sign in.

private static bool LookUpUser(string token, string userName)
{

	var request = new RestRequest("users", Method.GET);
	request.AddHeader("Authorization", token);

	request.AddQueryParameter("username", userName);

	var response = RestClient.Execute<List<UserModel>>(request);
	if (response.StatusCode == HttpStatusCode.OK)
	{
		//Ensure only 1 user is returned. The username should be unique so this is just a precaution. 
		return response.Data.Count == 1;
	}
	else
	{
		return false;
	}
}

The main addition here involves deserializing the response into a usermodel (https://myabsorb.com/api/rest/v1/Help/ResourceModel?modelName=UserModel). In this case we only need

  • DepartmentId
  • First Name
  • Last Name
  • Username
  • Password
  • EmailAddress

If the response returns one user then we’re set to proceed with the login. (If your setup doesn't use the username as the unique identifier, i.e. ExternalID, then you may need to include a check if duplicate users are found). If no user is found, the we must create one. The code to create a user is below,

private static bool CreateUser(string token, string username, Guid departmentId)
{
	var userModel = new UserModel()
	{
		DepartmentId = departmentId,
		EmailAddress = username + "@company.com",
		Username = username,
		Password = "ab5orb!ap1",
		FirstName = "Firstname",
		LastName = "Lastname"
	};

	var request = new RestRequest("users", Method.POST);

	request.AddHeader("Authorization", token);
	request.AddJsonBody(userModel);

	var response = RestClient.Execute(request);

	return response.StatusCode == HttpStatusCode.Created;
}

To start the user model is initialized with a default department. We have hard coded a department to make things simple but this can be automated however you see fit. We're also hard coding a password for simplicity but in practice, a random one should be generated for each new user. Since they’re signing in through SSO the password won’t impact them but is still required by the LMS.

If the user is created successfully this method will return true and pass that to VerifyUser which in turn passes that to our Initiate SSO method and proceeds with the login.

SSO Component

The next steps involve building a SAML Response and posting that to the target URL. To do this, we make use of a third-party package called ComponentSpace (http://www.componentspace.com/) with SAML2. Below is the entirety of the Initiate SAML method. We first define the target url which will be of the form https://company.myabsorb.com/account/saml then set our encryption methods. It’s currently suggested to use SHA-256.

private void InitiateSamlIdpSso(SsoViewModel model)
{
	var targetUrl = model.HostUrl + "/account/saml"; //Your absorb url

	var digestMethodForEncryption = "http://www.w3.org/2000/09/xmldsig#sha1";
	var signatureMethodForEncryption = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";

	var certificate = (X509Certificate2)HttpContext.Application["idpX509Certificate"];

	var issuerUrl = "http://localhost:51326/"; //Your idp url

	var samlResponseXml = SamlUtilities.GenerateSamlResponse(model.IdPropertyValue, targetUrl, issuerUrl, certificate, 
																	digestMethodForEncryption, signatureMethodForEncryption);

	IdentityProvider.SendSAMLResponseByHTTPPost(Response, targetUrl, samlResponseXml, model.RelayState);
}

A certificate is loaded along with defining the issuerUrl (which should just be your IdP url). Next, we gather all this up and pass it to a GenerateSamlResponse method. This method involves building the various attributes that make up the SAML response. The description of each attribute is described in detail in the SAML specification document found here (https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf).

public static XmlElement GenerateSamlResponse(string idPropertyValue, string targetUrl, 
	string issuerUrl, X509Certificate2 certificate, string digestMethodForEncryption, string signatureMethodForEncryption)
{
	var issuer = new Issuer(issuerUrl);

	var samlResponse = new SAMLResponse
	{
		Destination = targetUrl,
		Issuer = issuer,
		Status = new Status(SAMLIdentifiers.PrimaryStatusCodes.Success, null)
	};

	var subject = new Subject(new NameID(idPropertyValue));
	subject.SubjectConfirmations.Add(new SubjectConfirmation(SAMLIdentifiers.SubjectConfirmationMethods.Bearer)
	{
		SubjectConfirmationData = new SubjectConfirmationData { Recipient = targetUrl }
	});

We've mainly just included the attributes that are required by Absorb. The idPropertyValue should align with what has been set in Absorb such as Username, ExternalID, Email, etc. This is required by us so we can determine how to identify the user. Again, issuer is the IdP Url and target Url is your absorb portal + /account/saml. Next we build the SAML assertion and add it to the response,

var samlAssertion = new SAMLAssertion
	{
		Issuer = issuer,
		Subject = subject,
		Conditions = new Conditions
		{
			NotBefore = DateTime.Now,
			NotOnOrAfter = DateTime.Now.AddMinutes(5),
		},
		Statements = new List<IStatement>
		{
			new AuthnStatement
			{
				AuthnContext = new AuthnContext
						{
							AuthnContextClassRef = new AuthnContextClassRef(SAMLIdentifiers.AuthnContextClasses.Password)
						}
			}
		}
	};

	samlResponse.Assertions.Add(samlAssertion);

	// Sign the SAML response.
	var samlResponseXml = samlResponse.ToXml();
	SAMLMessageSignature.Generate(samlResponseXml, certificate.PrivateKey, certificate, null, digestMethodForEncryption, signatureMethodForEncryption);
	return samlResponseXml;
	

Finally, this is then passed back to the Initiate SAML SSO method which posts everything to Absorb. From there the user is redirected and if everything was done successfully the user is logged in.

Have more questions? Submit a request

0 Comments

Article is closed for comments.