Configuring a Rails App for Single Sign On with SAML Against Multiple Providers

What is Single Sign On?

Ever land on an app’s login page and feel that dread of needing to remember your credentials? Or needing to open your password manager 😉 ? Applications can also authorize you through external providers. You’ve seen the “Sign In With Facebook / Twitter / Google” buttons. That right there is an implementation of Single Sign On.

For a recent project, we were asked to implement Single Sign On so high schoolers could easily access our application when logged in to their external system. We’ll name our application ✏️ and the students’ external system 🔮  for reference later on. Let’s define some terms…

Single Sign On (SSO): Using one set of credentials to authenticate against related but individual applications.

Identity Provider (IdP): Holder of credentials, asserts to another system that a user is known and authenticated. This is generally Facebook, Twitter, Google, etc. For our example, this is 🔮 .

Service Provider (SP): Reads the token or cookies to allow authentication from the IdP. This is our app, ✏️ .

How does SSO work?

  1. User requests a service by visiting ✏️
  2. ✏️  requests an identity assertion from 🔮
  3. User enters credentials for 🔮 (Often this step will be unnecessary as the user is already logged in to 🔮)
  4. ✏️  obtains the confirmed identity assertion and authenticates the user


The ✏️  app is a standard Rails application using the Devise gem for authentication. ✏️  already provides a standard email and password login flow. To make authentication as easy as possible for future students, we will include a button to “Sign In With 🔮 ” on the login page.  

Two more (major) requirements were confirmed before development started. 1) We need to be able to switch out the 🔮  IdP with a different IdP at the time of the service request. This is because each state or even individual school may have their own IdP. 2) The SSO implementation must use the SAML protocol.

A couple quick definitions before discussing the solution:

Omniauth: a Ruby convention to abstract and generalize how to hook any authentication provider into Rack middleware.  For a Rails app, there are over 250 strategies listed, including Facebook, Twitter, Google, and SAML. We’ll be using SAML.

Security Assertion Markup Language (SAML): an XML-based data format for exchanging authentication information between an IdP and an SP. Because of the standard formatting, we are able to disassociate ✏️ implementation from the 🔮 implementation.  The platform doesn’t matter, SAML asserts the formatting. Here’s an example SAML document for authenticating a Service Provider against the Google SAML IdP:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="" validUntil="2022-01-25T22:44:33.000Z">
 <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
   <md:KeyDescriptor use="signing">
     <ds:KeyInfo xmlns:ds="">
   <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
   <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"


As with most Rails applications, the maturity of the ecosystem means a solution is available with very little configuration.  Check out the omniauth-saml gem, specifically the integration with Devise.

Referring to the gem’s documentation, you’ll notice that we have the option to use an idp_cert_fingerprint or an idp_cert.

Cert: Our example SAML document has a full cert, not a fingerprint, so we can plug that into the Devise setup. See the <ds:X509Certificate> tag.

Target URL: After playing around with some of the other data in the SAML document, we found that the URL listed in the <md:SingleSignOnService> tag is the appropriate value for this option.

Devise.setup do |config|
  config.omniauth :saml,
    idp_cert: 'cert',
    idp_sso_target_url: 'target_url'

The devise omniauth documentation will take you through an example using Facebook as a provider.  Following that structure, but replacing facebook with saml

Add gem “omniauth-saml” to the Gemfile.

Create a migration to add uid and provider to the users table. The uid field will allow us to match the user between the ✏️  and 🔮  systems.  The provider field will default to :saml.

Configure the User model by adding the :omniauthable option to the Devise options:

class User
  # omitting the rest of the class
  devise :omniauthable, omniauth_providers: [:saml]

This will expose the omniauth_authorize_path(:user, :saml) #=> /users/auth/saml route for starting the authorization with 🔮 .

We have to tell Devise in which controller we will implement the Omniauth callbacks, by adding an options to the routes.rb file. Then, we can define the saml action in the controller to read the request environment and find the user in ✏️ from the authorization details sent by 🔮 .

Rails.application.routes.draw do
  # omitting other routes
  devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  skip_before_action :verify_authenticity_token

  def saml
    auth_hash = request.env["omniauth.auth"]
    user = User.find_by(uid: auth_hash[:uid])
    # do stuff
    sign_in_and_redirect user

So… great! We can plug in those configuration options, expose a button on the login page, and the user flow would be as follows:

  1. Visit ✏️  login page
  2. Click button “Sign In With 🔮 ”, using omniauth_authorize_path(:user, :saml) path
  3. Enter credentials on 🔮
  4. Sent back to ✏️ , specificially the users/omniauth_callbacks#saml action
  5. Update data, check user status, etc.
  6. ✏️  signs in the user using Devise’s sign_in_and_redirect user method

Are we done??

….. almost there. One of the requirements of the system was to dynamically send the user to different IdPs depending on their state / school. Meaning, in some cases we’ll need to authenticate against the 🔮 IdP, but for other users we may need to authenticate against a different IdP.

This means that we cannot configure the IdP with the Devise initializer. Instead, we need to set up request-time modification to change the cert and target_url for each auth request. Reading through the omniauth code, you’ll see a method setup_phase, called before the request processes.  By passing in a proc (or anything that response to .call) to the setup: option, we can configure which auth provider to use before the request:

require “omniauth_saml_setup”

Devise.setup do |config|
  # omitting the rest of the config block
  config.omniauth :saml, setup: OmniauthSamlSetup
class OmniauthSamlSetup
  # Omniauth expects the class passed to setup to respond to the #call method.
  # env - Rack environment
  # This class is Rack middleware, we put it in the "lib/" directory

  def initialize(env)
    @env = env

  def setup


  def saml_settings
    # find your provider, given a subdomain or a query param
    provider = Provider.find_by(foo: params[:bar])
      idp_cert: "-----BEGIN CERTIFICATE-----\n#{provider.cert}\n-----END CERTIFICATE-----",
      idp_sso_target_url: provider.target_url

🎉  🎉  🎉  🎉  🆒  🆒  🆒  🆒


DevMynd is custom software development company in Chicago and San Francisco with practice areas in digital strategy, UI/UX, and custom mobile and web application development.

Matt is a member of DevMynd’s software engineering team focusing on mobile apps and web development. He has been with the company since 2015.