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.

Installation

gem 'devise_saml_authenticatable'
  • Execute bundle install:
bundle install

Usage

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|
      user.email = attributes['email']
    end
    admin_user.save!
    admin_user
  end

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:

"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "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 = OneLogin::RubySaml::Authrequest.new
    action = request.create(saml_config, {'RelayState' => 'https://your.application.com/admin/saml/auth'})
    redirect_to action
  end

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

Set the new routes in config/routes.rb:

Rails.application.routes.draw do
  devise_config = ActiveAdmin::Devise.config
  # see https://github.com/activeadmin/activeadmin/wiki/Log-in-through-OAuth-providers for this use
  devise_config[:controllers][:saml_sessions] = 'saml_sessions'
  devise_for :admin_users, devise_config  
  ActiveAdmin.routes(self)

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= 'https://your.application.com/admin/saml/auth'
    settings.issuer = 'https://your.application.com/admin/saml/metadata'  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
    # https://github.com/onelogin/ruby-saml#metadata-based-configuration
    idp_metadata = OneLogin::RubySaml::IdpMetadataParser.new.parse_remote('https://adfs.yourcompany.com/federationmetadata/2007-06/federationmetadata.xml')
    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 = File.read('sp_private.key') if File.exist?('sp_private.key')

    # certificate of sp
    settings.certificate = File.read('sp_certificate.pem') if File.exist?('sp_certificate.pem')

    settings.security[:authn_requests_signed] = true
    settings.security[:logout_requests_signed] = true
    settings.security[:logout_responses_signed] = true
    settings.security[:metadata_signed] = true
    settings.security[:digest_method] = XMLSecurity::Document::SHA1
    settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
    settings.security[:embed_sign] = false  
  end

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 https://your.application.com/admin/saml/metadata.

Add the newly created sp to ADFS (see https://technet.microsoft.com/en-us/library/dd807132(v=ws.11).aspx 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 https://www.samltool.com a great tool for this.


0 comments:

Post a Comment