Implementing a Custom SAML and User Provisioning Solution

Disclaimer

We reserve the right to update our (Service Provider) use of the relevant elements in accordance with the SAML 2.0 specifications as part of our release process and as such the client will assume the responsibility for ensuring their (Identity Provider) implementation follows the said standard.

The information provided in this article was written prior to Absorb implementing Just-In-Time (JIT) User Provisioning for SAML 2.0. Today, we recommend using the SSO User Provisioning feature with SAML 2.0 if that is what you want to achieve. Learn more here.

Clients using a standard Identity Provider do not usually need to write any code in order to implement SAML 2.0. You can read more about SAML 2.0, including Identity Providers our clients have used here.

This article describes an example implementation of user provisioning via Absorb’s RESTful API, as well as a way to implement SAML 2.0. For the purposes of this article, we'll assume a basic understanding of the SSO process along with some background in development. 

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.

Code Troubleshooting

Absorb does not provide support for writing, debugging, or troubleshooting client code.

Custom SAML Identity Provider

In the context of implementing your own Identity Provider (IdP), there are important SAML data elements that should be included in your SAML assertions for the SSO flow to work. It is expected for your IdP to follow the official SAML 2.0 specification, and the Web Browser SSO profile:

Below are the important elements that are required for the following scenarios supported by Absorb.

Important Fields for SAML Assertion

These fields are essential when interacting with an SAML Assertion:

Field Description
Assertion The package element of information with one or more statements
ID Identifier for this assertion
IssueInstant Time that the instant is issued in UTC.
Version The SAML version for this assertion, should be “2.0”
Issuer The authority making the claim in this assertion, should be the URI for your IdP.
<ds: Signature> Contains the XML Signature that protects the integrity of this assertion
Subject The package element for the subject’s (user) identity
NameId The ID Property identifier as configured in Absorb SSO settings
Conditions Contains information that denotes the validity of the assertion
NotBefore A time instant in UTC
NotOnOrAfter A time instant in UTC
AuthnStatement Contains information of the assertion subject
AuthnContext The context of the authentication event

 

Important Fields for Incoming Single Sign-On (SSO)

These fields are essential when interacting observing Incoming Single Sign-On (SSO):

Field Description
SAML Assertion All the elements as denoted above

 

Important Fields for Outgoing SSO

For a SAML Authentication Request (AuthnRequest) sent to the LMS, the following elements are required:

Field Description
ID Identifier for this request
IssueInstant Time that the instant is issued in UTC.
Version The SAML version for this assertion, should be “2.0”
Issuer The authority making the claim in this assertion, should be the URI for your IdP
ProtocolBinding The protocol for sending the authentication response, should be HTTP-POST
<ds: Signature> Contains the XML Signature that protects the integrity of this assertion

 

Important Fields for Single Logout (SLO)

For a SAML Authentication Response (SAML Response) sent to the LMS, the following elements are required:

Field Description
SAML Assertion All the elements as denoted above
SessionIndex Part of the AuthStatement, contains a session identifier

 

For a SAML Logout Response (LogoutResponse) sent to the LMS, the following elements are required:

Field Description
Issuer The authority making the claim in this assertion, should be the URI for your IdP
<ds: Signature> Contains the XML Signature that protects the integrity of this assertion
Status Contains the StatusCode

 

Important Fields for Account Provisioning

For a SAML Authentication Response sent to the LMS, the following elements are required:

Field Description
SAML Assertion All the elements as denoted above
AttributeStatement   

 

The SAML Authentication response must contain the following attributes:

Field Description
Username The username. Must be unique, and between 1 and 255 characters
FirstName The user's first name. Must be between 1 and 255 characters
LastName The user's last name. Must be between 1 and 255 characters
DepartmentId* A GUID that matches a department's GUID within Absorb.
ExternalDepartmentId* Matches a department's external Id within Absorb.

 

Provisioning Advisory

User Account Provisioning only requires DepartmentId or ExternalDepartmentId; not both. Additional custom fields can be discovered in this article: here.

User Provisioning Implementation Example

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.

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 log in. 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 log in 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(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, then 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 = " ";
var signatureMethodForEncryption = " ";
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.

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 {
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.

 

Was this article helpful?
0 out of 0 found this helpful