Skip to content

Setting Up with Multiple Providers

Lou Montulli edited this page May 7, 2024 · 9 revisions

To setup multiple providers start by following the getting started guide. The following is a greatly reduced adaption of our implementation. Okta and Onelogin samples are included at the end of the wiki.

gem 'devise_saml_authenticatable'

bundle install

User (user.rb)

devise :saml_authenticatable, :trackable

This wiki assumes your devise model is called User

Devise initializer (devise.rb):

require 'id_p_settings_adapter'
Devise.setup do |config|
    config.saml_create_user = true
    config.saml_update_user = true
    config.saml_default_user_key = :email
    config.saml_session_index_key = :session_index
    config.saml_use_subject = true
    config.idp_settings_adapter = IdPSettingsAdapter
    config.saml_configure do |settings|
        settings.assertion_consumer_service_url     = ""
        settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        settings.name_identifier_format             = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
        settings.issuer                             = ""
        settings.authn_context                      = ""
        settings.idp_slo_target_url                 = ""
        settings.idp_sso_target_url                 = ""
        settings.idp_cert_fingerprint               = ''
        settings.idp_cert_fingerprint_algorithm     = 'http://www.w3.org/2000/09/xmldsig#sha256'
end

Note the idp_settings_adapter statement. We will come back to this soon.

We are providing a few defaults (assertion_consumer_service_binding, name_identifier_format, and idp_cert_fingerprint_algorithm) here for your customers. It could be helpful to document these and provide them to your customers. It can help them set up their IdP provider application.

Attribute Mappings (attribute-map.yml)

"email": "email" # key is the IdP name and value is the column it is mapped to in your application
"lastName": "last_name"
"firstName": "first_name"

Again, you will need to document and tell your users to add email as an attribute or parameter depending on their setup. This is based on the saml_default_user_key setting in devise.rb above. Other attributes are optional and depend on what makes instances of your User model valid. Our implementation has a first name and last name validation, so we require it from our customer's provider.

Multiple Settings

For allowing users to self service their own IDP we use a tenancy strategy, but you can do this however you want.

IdPSetup (idp_setup.rb)

We have a IdPSetup model with the following attributes:

  • id
  • assertion_consumer_service_url
  • assertion_consumer_service_binding
  • name_identifier_format
  • issuer
  • idp_entity_id
  • authn_context
  • idp_slo_target_url
  • idp_sso_target_url
  • idp_cert_fingerprint
  • organization_id
  • idp_cert_fingerprint_algorithm
  • created_at
  • updated_at

This model belongs to an organization. We reference the organization later and use these attributes to override the settings in the devise.rb initializer.

For a bare bones setup, at the minimum, you must have:

  • idp_sso_target_url # where the user is redirected to for login at the IdP
  • idp_cert_fingerprint # for authentication - https://www.samltool.com/fingerprint.php is helpful for this
  • idp_entity_id # This is used to find the idp_setup and organization when the provider sends the user back. Entity ID, or SAML Issuer ID would go here to be referenced later. This is automatically passed into the idp_settings_adapter from the SAML payload OR from your overridden aions in saml_sessions_controller.rb (see below). As an example, in Okta, they call it the SAML Issuer ID under 'Advanced Settings' when in the Edit SAML Integration view.

IdPSettingsAdapter (id_p_settings_adapter.rb)

  • Next, create a file called id_p_settings_adapter.rb in your lib directory.
class IdPSettingsAdapter
    def self.settings(idp_entity_id)
      # Find your settings. Here we identify our tenant (Organization class)
      tenant = IdpSetup.find_by_idp_entity_id(idp_entity_id).organization
      # Urls below are for a development enviroment
      if tenant.present? 
          {
            assertion_consumer_service_url: "http://#{tenant.short_name.parameterize}.localhost:3000/auth",
            assertion_consumer_service_binding: tenant.idp_setup.assertion_consumer_service_binding.present? ? tenant.idp_setup.assertion_consumer_service_binding : "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
            name_identifier_format: tenant.idp_setup.name_identifier_format.present? ? tenant.idp_setup.name_identifier_format : "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
            issuer: "http://#{tenant.short_name.parameterize}.localhost:3000/metadata",
            authn_context: "",
            idp_slo_target_url: "",
            idp_sso_target_url: tenant.idp_setup.idp_sso_target_url,
            idp_cert_fingerprint: tenant.idp_setup.idp_cert_fingerprint,
            idp_cert_fingerprint_algorithm: tenant.idp_setup.idp_cert_fingerprint_algorithm.present? ? tenant.idp_setup.idp_cert_fingerprint_algorithm : 'http://www.w3.org/2000/09/xmldsig#sha256'
        }
    else
        {}
    end
  end
end

In the above example, you must update your assertion_consumer_service_url & issuer for your production environment.

Routes (routes.rb)

In the routes.rb file, we will override much of the default behavior and enable the use of a custom SamlSessionsController.

  ...
devise_for :users, skip: [:registrations, :saml_authenticatable], controllers: { sessions: 'user/sessions' }
    as :user do
    get 'users/edit' => 'devise_registrations#edit', as: 'edit_registration'
    patch 'users' => 'devise_registrations#update', as: 'registration'

# To avoid conflict, saml_authenticatable routes are manually defined here.
# Also we override the controller.

    resource :session, as: 'saml_session', only: [], controller: 'saml_sessions', path: '/' do
      get :new, path: 'sign_in', as: 'new'
      match :destroy, path: 'sign_out', as: 'destroy', via: :get
      post :create, path: 'auth'
      get :metadata, path: 'metadata'
      match :idp_sign_out, path: 'idp_sign_out', via: [:get, :post]

    end
end
...

SamlSessionsController (saml_sessions_controller.rb)

In the saml_sessions_controller.rb we will define the idp_setup that will be sent to the idp_settings_adapter for use in the new action. We also use the :store_winning_strategy provided in one of the issue resolutions.

In the create session, you can add attributes specific to your application if needed.

class SamlSessionsController < Devise::SamlSessionsController
    after_filter :store_winning_strategy, only: :create

    def new
        request = OneLogin::RubySaml::Authrequest.new
        action = request.create(saml_config(current_tenant.idp_setup.idp_entity_id))
        redirect_to action
    end

    def create
        if current_user.type.blank?
           current_user.update(type: "SampleUser")
        end

        super
    end

    private

    def store_winning_strategy
        warden.session(:user)[:strategy] = warden.winning_strategies[:user].class.name.demodulize.underscore.to_sym
    end
end

Working Okta

The idp_setup

Image Deleted from S3, unfortunately.

The Okta setup

Image Deleted from S3, unfortunately.

One Login

The idp_setup

Image Deleted from S3, unfortunately.

The One Login Setup

Images Deleted from S3, unfortunately.