fbpx

Dynamic SAML idP Provisioning for SaaS Rails Apps

Dynamic SAML idP Provisioning for SaaS Rails Apps
Reading Time: 5 minutes

A client came to us with a request to integrate Single Sign On (SSO) into their already existing web application with Security Assertion Markup Language (SAML). Our client has a SaaS application where they provide services to the manufacturing process labs of petroleum products providers, using a default authentication scheme provided by Devise. Some of these labs began to request the ability to authenticate their own credentials using SSO for both security and convenience.  SSO is useful because it allows an Identity Provider (idP) like Google Workspace idP, Microsoft Azure Active Directory, or Okta to authenticate a user trying to access a web application instead of the web application handling authentication itself. A normal SSO flow would look like this: 

Integrating SSO would be a straightforward task if not for the caveat that each of our client’s customers could in theory have a different idP that they use to authenticate their users. Each different idP would have different settings needed to authenticate their users. Most SAML configurations we researched for integrating into Rails applications had their idP setup credentials and settings hard coded into a config file with the expectation that only a static number of idP’s would need to be used. For our purposes we needed to:

  1. Support one independent idP per lab and allow idP credentials to be entered by our client or our client’s labs through the web application, and save those credentials in our database.
  2. Dynamically evaluate the users logging in at runtime and direct them to their idP based on the lab they belonged to. Then receive the SAML authentication and log them into the application. If the users did not opt-in to use SSO with SAML we would authenticate them through the application as we have been. An example of our desired scheme is pictured below:

After researching we decided to use the Devise SAML Authenticatable gem as our jumping-off point. It would integrate with our current Devise setup and we would only need to override a few configurations.

Step 1: idP Provisioning Configuration Setup

During authentication, the idP returns an assertion (formatted as an XML document) which provides the status of the authenticated user. We found that the following three fields are always returned and will vary between idPs

  1. idP Certificate (idp_cert)
  2. idP Entity ID (idp_entity_id)
  3. idP SSO Target Url (idp_sso_target_url). 

We added these fields to our labs database table, along with a boolean active_saml attribute and a defaulted authn_context field that certain idP’s require. To make things simple for the users we made it so that the three fields listed above are the only fields that they are required to include on their lab’s setup page. They’ll activate SAML for their lab by checking a box (connected to active_saml) and populating all three fields. To deactivate SAML they simply uncheck the box. 

Step 2: Dynamic idP Provisioning and SAML Authentication Integration

To set up the actual SAML connections, we first followed the initial steps in the devise_saml_authenticatable gem. This included modifying our Devise initializer in config/initializers/devise.rb to include the defaults listed in the gem and creating an attribute map at config/attribute-map.yml to map to the email attribute that we have on the users database table: 

"urn:mace:dir:attribute-def:email": "email"

Next we had to point our login to a new authentication path. Once we verified the email and the lab that the user belonged to, if that lab had checked the active_saml attribute, we redirected to devise_saml_authenticatable’s default path, passing a lab_id in the parameters. From there, the gem works its way to the Devise Entity Reader and the Devise Settings Adapter. These are both options in the Devise initializer, and here is where we had to start customizing. 

We added a module called SamlAuth with two classes inside: SamlEntityFinder and SamlAuth. The Entity Reader, linked above, was derived from devise_saml_authenticatable’s gem, but we added some customization. The settings adapter we created based on this comment in the Devise initializer:

You can support multiple idPs by setting this value to the name of a class that implements a ::settings method which takes an idP entity id as an argument and returns a hash of idP settings for the corresponding idP.

Starting with the Entity Reader, when the lab_id is passed, if it is present, we know that we are currently sending a request for authentication. We pass the lab_id and an override variable in place of an idp_entity_id to the settings method. If the lab is found by the lab_id, we create a settings object containing idp_entity_id, idp_sso_target_url,idp_cert, and the defaulted authn_context. Upon getting a response, the Entity Reader is called again but this time we are looking for the SAMLResponse. We proceed here as the devise_saml_authenticatable gem had defined, and allow the Lab to be found by the actual idp_entity_id instead of our overridden object from before. From here, we allow the gem to work as intended, logging our user in with our SAML credentials authenticated at runtime.

module SamlAuth
  class SamlEntityFinder
    def self.entity_id(params)
      if params[:lab_id]
        {
          lab_override: true, lab_id: params[:lab_id]
        }
      elsif params[:SAMLResponse]
        {
          lab_override: false, entity_id: 
            OneLogin::RubySaml::Response.new(
              params[:SAMLResponse],
              settings: Devise.saml_config,
              allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
            ).issuers.first
        }
      end
    end
  end
 
  class SamlAuth
    def self.settings(idp_entity_id)
      lab = idp_entity_id[:lab_override].present? ?
        Lab.find(idp_entity_id[:lab_id]) :
        Lab.find_by_idp_entity_id(idp_entity_id[:entity_id])
      if lab 
        {
          idp_entity_id: lab.idp_entity_id,
          authn_context:  lab.authn_context,
          idp_sso_target_url:  lab.idp_sso_target_url,
          idp_cert: lab.idp_cert
        }
      else 
        {}
      end
    end
  end
end

After development there were a few more steps to take. We tested users through Google WorkSpace idP and our client’s Azure Active Directory. Afterward, we put together a document that our client could distribute to their labs that would detail exactly how to set up SAML on their end and how to connect it to our client’s application. We are happy to report that this new authentication scheme has already been adopted by many of our client’s customers’ labs and they’ve been able to set it up seamlessly, without having to contact either the client or us. 

New call-to-action

About Mission Data

We’re designers, engineers, and strategists building innovative digital products that transform the way companies do business. Learn more: https://www.missiondata.com.