Integrate SAML with many IDPs with Devise & OmniAuth

Vijay Kumar Agrawal

By Vijay Kumar Agrawal

on October 11, 2017

Recently, we integrated our SAML service provider(SP) with multiple identity providers(IDPs) to facilitate Single sign-on(SSO) using Devise with OmniAuth.

Before we jump into the specifics, here is SAML definition from wikipedia.

Security Assertion Markup Language (SAML, pronounced sam-el) is an open standard for exchanging authentication and authorization data between parties, in particular, between an identity provider(IDP) and a service provider(SP).

The choice of Devise with OmniAuth-SAML to build SAML SSO capabilities was natural to us, as we already had dependency on Devise and OmniAuth nicely integrates with Devise.

Here is the official overview on how to integrate OmniAuth with Devise.

After following the overview, this is how our config and user.rb looked like.

1# config file
2Devise.setup do |config|
3  config.omniauth :saml,
4    idp_cert_fingerprint: 'fingerprint',
5    idp_sso_target_url: 'target_url'
6end
7
8#user.rb file
9devise :omniauthable, :omniauth_providers => [:saml]

The problem with above configuration is that it supports only one SAML IDP.

To have support for multiple IDPs, we re-defined files as below.

1# config file
2Devise.setup do |config|
3  config.omniauth :saml_idp1,
4    idp_cert_fingerprint: 'fingerprint-1',
5    idp_sso_target_url: 'target_url-1'
6    strategy_class: ::OmniAuth::Strategies::SAML,
7    name: :saml_idp1
8
9  config.omniauth :saml_idp2,
10    idp_cert_fingerprint: 'fingerprint-2',
11    idp_sso_target_url: 'target_url-2'
12    strategy_class: ::OmniAuth::Strategies::SAML,
13    name: :saml_idp2
14end
15
16#user.rb file
17devise :omniauthable, :omniauth_providers => [:saml_idp1, :saml_idp2]

Let's go through the changes one by one.

1. Custom Providers: Instead of using standard provider saml, we configured custom providers (saml_idp1, saml_idp2) in the first line of configuration as well as in user.rb

2. Strategy Class: In case of the standard provider(saml), Devise can figure out strategy_class on its own. For custom providers, we need to explicitly specify it.

3. OmniAuth Unique Identifier: After making the above two changes, everything worked fine except OmniAuth URLs. For some reason, OmniAuth was still listening to saml scoped path instead of new provider names saml_idp1, saml_idp2.

1# Actual metadata path used by OmniAuth
2/users/auth/saml/metadata
3
4# Expected metadata path
5/users/auth/saml_idp1/metadata
6/users/auth/saml_idp2/metadata

After digging in Devise and OmniAuth code bases, we discovered provider name configuration. In the absence of this configuration, OmniAuth falls back to strategy class name to build the path. As we could not find any code in Devise which defined name for OmniAuth that explained saml scoped path (we were expecting Devise to pass name assigning same value as provider).

After adding name configuration, OmnitAuth started listening to the correct URLs.

4. Callback Actions: Lastly, we added both actions in OmniauthCallbacksController:

1class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
2
3  def saml_idp1
4    # Implementation
5  end
6
7  def saml_idp2
8    # Implementation
9  end
10
11  # ...
12  # Rest of the actions
13end

With these changes along with the official guide mentioned above, our SP was able to authenticate users from multiple IDPs.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.