Thursday, 23 June 2016

Implementing Single-Sign-On with ADFS 3.0 in an ActiveAdmin Ruby on Rails application

My company uses ADFS 3.0 to provide Single-Sign-On possibilities for its employees to make use of Microsoft's Office 365 solutions. It also has a couple of web applications in Ruby on Rails which can be used by the employees to do several tasks for the business.
Until now those RoR applications have their own login and user administrations. This provides a security risk. You need to go by all those applications, one by one, to remove or disable accounts of employees who left the company, to make sure they cannot log in anymore on those applications. So we started to replace those user account administration with the same SSO possibilities as the Office 365 applications. When that is complete, you only have to disable the account of the employee on a single point, in Active Directory, and ADFS will make sure he or she cannot log in anymore in any of the applications.

I've found a gem which, with a little configuration, can be used to do exactly that. DeviseSamlAuthenticatable is a Single-Sign-On authentication strategy for Devise that relies on SAML. It uses ruby-saml to handle all the SAML related things.


gem 'devise_saml_authenticatable'
  • Execute bundle install:
bundle install


In app/models/<YOUR USER MODEL>.rb set the :saml_authenticatable strategy. In a default ActiveAdmin implementation the model is admin_user.rb:

class AdminUser < ActiveRecord::Base
  # Include devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :omniauthable
  # :database_authenticatable, :recoverable, :rememberable,
  # :trackable, :validatable
  devise :saml_authenticatable, :trackable

And add a method to load the SAML data into the user, also in admin_user.rb:

  def self.load_saml_data attributes
    admin_user = where(email: attributes['email']).first_or_create do |user| = attributes['email']

In the config directory create a YAML file (config/attribute-map.yml) which will contain the mappings from the attributes in the SAML response of ADFS to the attributes used in admin_user.rb:

"": "email"

Add a SAML sessions controller in app/controller/saml_sessions_controller.rb:

class SamlSessionsController < Devise::SamlSessionsController
  skip_before_filter :verify_authenticity_token

  def new
    request =
    action = request.create(saml_config, {'RelayState' => ''})
    redirect_to action

  def create
      response =[:SAMLResponse], settings: saml_config)
      if response.is_valid?
        attribute_map = YAML.load("#{Rails.root}/config/attribute-map.yml"))
        attributes = Hash[ { |k, v| [attribute_map[k], v[0]] }]
        @user = AdminUser.load_saml_data attributes
        session[:userid] =
        if @user.persisted?
          flash[:notice] = 'Signed in successfully.'
          sign_in_and_redirect @user, :event => :authentication
        raise 'Invalid response'
    rescue Exception => e
      flash[:notice] = e.message
      redirect_to root_path

Set the new routes in config/routes.rb:

Rails.application.routes.draw do
  devise_config = ActiveAdmin::Devise.config
  # see for this use
  devise_config[:controllers][:saml_sessions] = 'saml_sessions'
  devise_for :admin_users, devise_config  

In config/initializers/devise.rb add the configuration settings for saml authenticatable:

  # ==> Configuration for :saml_authenticatable
  # Create user if the user does not exist. (Default is false)
  config.saml_create_user = true

  # Update the attributes of the user after a successful login. (Default is false)
  config.saml_update_user = true

  # Set the default user key. The user will be looked up by this key. Make
  # sure that the Authentication Response includes the attribute.
  config.saml_default_user_key = :email

  # Optional. This stores the session index defined by the IDP during login. If provided it will be used as a salt
  # for the user's session to facilitate an IDP initiated logout request.
  config.saml_session_index_key = :session_index

  config.saml_use_subject = true

  # Configure with your SAML settings (see [ruby-saml][] for more information).
  config.saml_configure do |settings|
    settings.assertion_consumer_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
    settings.assertion_consumer_logout_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'

    # sp settings
    settings.assertion_consumer_service_url= ''
    settings.issuer = ''  settings.authn_context = ''
    settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'

    # Optional for most SAML IdPs  
    settings.authn_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
    # X509 certificate of IDP to validate saml response
    idp_metadata ='')
    settings.idp_cert = idp_metadata.idp_cert
    settings.idp_sso_target_url = idp_metadata.idp_sso_target_url
    settings.idp_slo_target_url = idp_metadata.idp_slo_target_url

    # Private key of sp  
    settings.private_key ='sp_private.key') if File.exist?('sp_private.key')

    # certificate of sp
    settings.certificate ='sp_certificate.pem') if File.exist?('sp_certificate.pem')[:authn_requests_signed] = true[:logout_requests_signed] = true[:logout_responses_signed] = true[:metadata_signed] = true[:digest_method] = XMLSecurity::Document::SHA1[:signature_method] = XMLSecurity::Document::RSA_SHA1[:embed_sign] = false  

Generate a new self-signed certificate for your newly created Service Provider:
openssl req -x509 -newkey rsa:2048 -keyout sp_private.key -out sp_certificate.pem -days 3650 -nodes
While your application is running, the metadata of the SP should now be available at

Add the newly created sp to ADFS (see for help how to do that).

Some extra help

When I tried implement everything I had some trouble with choosing the correct certificate which I should use for the IDP, and whether I got the correct attributes in my SAML response from the ADFS. I found a great tool for this.


Post a Comment