Page tree
Skip to end of metadata
Go to start of metadata

This document will provide a summary on how to integrate a Rails 5.1 application with UBC's Identity provider.

The integration of SAML based authentication into devise is provided by the devise_saml_authenticatable gem which is based on the ruby-saml gem from OneLogin. 

Application Environment

This guide assumes that a Rails 5.1 app is to be integrated with the UBC Identity provider.  

The existing or new application uses the devise gem as an authentication solution and the devise model is called User.

Ensure that your server time is synchronized with ntp

Development Tips

The chrome extension "SAML Chrome Panel" was useful to inspect SAML requests during development

A Rails IdP was used for initial configuration testing and can be found at here https://github.com/sportngin/saml_idp


Step-by-step guide

  1. Gems to install in app Gemfile

    Gemfile
    gem 'devise'
    gem 'devise_saml_authenticatable'
    gem 'figaro'
    bundle install
  2. Create and fetch certificates and keys - you can do this as you wish, but here is a quick generator, fauthentic is simply a wrapper around OpenSSL
    1. Install additional gems - not required for rails app

      gem install fauthentic ruby-saml awesome_print
    2. Run the following Ruby script to generate the certificates and key - fill in the <DETAILS>

      makekey.rb
      require 'yaml'
      require 'fauthentic'
      require 'ruby-saml'
      require 'awesome_print'
      require 'openssl'
      
      OneLogin::RubySaml::IdpMetadataParser.class_eval do
       def certs
        @certificates
       end
      end
      
      opts = {
        common_name: "<YOUR SERVER NAME>", 
        country: "CA",                	
        state: "BC",            		
        org: "<DEPARTMENT>",                 	
        org_unit: "<GROUP>",             
        email: "<EMAIL>",
        expire_in_days: 5*365
      }
      ssl = Fauthentic.generate(opts)											
      cert_string = ssl.cert.to_pem
      key_string = ssl.key.to_s
      
      # Fetch metadata from ubc
      metadata_url = "https://authentication.stg.id.ubc.ca/idp/shibboleth"		#use the staging or production idp metadata 
      #metadata_url = "https://authentication.ubc.ca/idp/shibboleth"
      
      settings = OneLogin::RubySaml::IdpMetadataParser.new
      settings.parse_remote_to_hash(metadata_url)
      ap settings.certs  															#see all the certs from the idp metadata
      selected_cert = settings.certs['signing'][1] 								#this is the one the idp appears to use!
      idp_cert = OpenSSL::X509::Certificate.new(Base64.decode64(selected_cert)).to_pem
      
      # write to yaml file
      secrets = { 'shared' => {'cert' => cert_string, 'private_key' => key_string, 'idp_cert' => idp_cert }}
      File.open('saml_secrets.yml','w'){ |f| f.write secrets.to_yaml }

      This generates a saml_secrets.yml file containing the certificates and keys. 

      It is not recommended to add saml_secrets.yml to your app repository - the certs will be copied into the correct locations later

  3. Create initializers in config/initializers
    1. As described in the devise_saml_authenticatable gem, Devise is configured by adding additional settings in the devise initialiser file created by the devise:install task. 

      config/initializers/devise.rb
      Devise.setup do |config|
          #....
          # other configuration 
          #....
          config.saml_create_user = false 
          config.saml_update_user = true
          config.saml_default_user_key = :uid
          config.saml_session_index_key = :session_index
          config.saml_use_subject = false
          config.idp_settings_adapter = nil
          config.saml_sign_out_success_url = Figaro.env.slo_success_url
          config.allowed_clock_drift_in_seconds = 1.second
      
          config.saml_configure do |settings|
            settings.assertion_consumer_service_binding 	= Figaro.env.service_binding
            settings.assertion_consumer_service_url     	= Figaro.env.base + "/users/saml/auth"    
            settings.assertion_consumer_logout_service_url    = Figaro.env.base + "/users/saml/idp_sign_out"
           
            settings.issuer                             = Figaro.env.base
            settings.authn_context                      = ""
            settings.idp_slo_target_url                 = Figaro.env.idp_slo_target_url
            settings.idp_sso_target_url                 = Figaro.env.idp_sso_target_url
            settings.idp_cert                           = Figaro.env.idp_cert
      	
            settings.protocol_binding			  = Figaro.env.service_binding
            settings.force_authn			  = 1
      
            settings.certificate = Rails.application.secrets.cert
            settings.private_key = Rails.application.secrets.private_key
            
            settings.security[:authn_requests_signed]   = false    # Enable or not signature on AuthNRequest
            settings.security[:logout_requests_signed]  = true     # Enable or not signature on Logout Request
            settings.security[:logout_responses_signed] = false    # Enable or not signature on Logout Response
            settings.security[:want_assertions_signed]  = true     # Enable or not the requirement of signed assertion
            settings.security[:metadata_signed]         = true     # Enable or not signature on Metadata
            settings.security[:want_assertions_encrypted] = true
      
            settings.security[:digest_method]    = XMLSecurity::Document::SHA1
            settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
          end
      end

      More complex, multi IDP configurations are possible but are beyond the scope of this how-to guide

    2. These settings determine if the user should be added to your app on successful sign in. My app is set to false, as I only want known users to have access. The uid of these users must be added to the db.

      Any changes in user data will be updated on sign in.

      config.saml_create_user = false 
      config.saml_update_user = true
  4. Setup app environmental variables
    1. Here we use the Figaro gem to handle environment variables.  The installer creates a config/application.yml file

      bundle exec figaro install
    2. Add the following configuration to application.yml.  Note that the idp_sso_target_sso_url is set for the staging instance here. 

      config/application.yml
      service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
      
      production:
        base: <BASE URL OF YOUR APPLICATION>
        idp_base: https://authentication.stg.id.ubc.ca
        idp_sso_target_url: https://authentication.stg.id.ubc.ca/idp/profile/SAML2/Redirect/SSO
        idp_slo_target_url: https://authentication.stg.id.ubc.ca/idp/profile/SAML2/Redirect/SLO
        slo_success_url: <URL ON LOGOUT>
        idp_cert: |
          -----BEGIN CERTIFICATE-----
          PUT THE IDP CERT HERE (see saml_secrets file or get from shib metadata)
          -----END CERTIFICATE-----
      development:
        base: http://localhost:3001
        idp_base: http://localhost:3011
        idp_sso_target_url: http://localhost:3011/saml/auth
        idp_slo_target_url: http://localhost:3011/saml/auth
        slo_success_url: http://localhost:3001
        idp_cert: |
          -----BEGIN CERTIFICATE-----
          YOU MAY USE A DIFFERENT IDP FOR DEV
          -----END CERTIFICATE-----
      
      test:
        base: http://localhost:3001
        idp_base: http://localhost:3011
        idp_sso_target_url: http://localhost:3011/saml/auth
        idp_slo_target_url: http://localhost:3011/saml/auth
        slo_success_url: http://localhost:3001
        idp_cert: |
          -----BEGIN CERTIFICATE-----
          YOU MAY USE A DIFFERENT IDP FOR TEST
          -----END CERTIFICATE-----
    3. Add cert and key to secrets.yml from saml_secrets.yml or from own method

      config/secrets.yml
      shared:
        cert: |
          -----BEGIN CERTIFICATE-----
          YOUR CERT GOES HERE
          -----END CERTIFICATE-----
        private_key: |
          -----BEGIN RSA PRIVATE KEY-----
          YOUR KEY GOES HERE
          -----END RSA PRIVATE KEY-----
      
      
  5. Configuration of models and attributes
    1. Add uid column to the User model.  A suitable migration would be:

      add_uid_to_users.rb
      class AddUidToUsers < ActiveRecord::Migration[5.1]
        def change
          add_column :users, :uid, :string
          add_index :users, :uid
        end
      end
    2. Add saml_authenticable module to User model

      app/models/user.rb
      class User < ApplicationRecord
      	devise :saml_authenticatable
       
      end
    3. Add an attribute-map.yml file which matches the data sent by the IdP to the model column names.  The attribute names are available elsewhere on this site. The IdP must be configured by the administrator to allow your app access to these attributes.

      config/attribute-map.yml
      "urn:oid:0.9.2342.19200300.100.1.1" : "uid"
      "urn:oid:0.9.2342.19200300.100.1.3" : "email" 
      "urn:oid:2.5.4.42" : "first_name"
      "urn:oid:2.5.4.4"  : "last_name"

      TODO: 

      1. Note The devise encrypted password column is not required now
      2. Note about debug of these attributes with encrypted attributes 

  6. Collecting information for integration with IdP
    1. The IdP requires metadata from the SP configuration.  Run your app server, and navigate to '/users/saml/metadata' to retrieve the metadata.  Pass this xml file onto the IdP administrator.  Any changes to the internal configuration may cause the metadata to change. 
  7. Sign In and Out
    1. The devise routes 'new_user_session' will point to the '/user/saml/sign_in' uri so should not require any configuration changes to existing links.  
    2. The CWL icon should be used for login buttons. Add this link to your main page somewhere:

      view for login (this is haml)
      %p 
        = t('login_message')
      = link_to image_tag('CWL_login_button.gif'), new_user_session_path
    3. Download the button and put it in app/assets/images
  8. Configuration for Local Application Authentication Failures (optional)
    1. If your application is configured so that only locally registered users can sign in you will have the configuration
      config.saml_create_user = false 

      On successful CWL authentication the SAML response is returned and the user is looked up in the local database. If the user is not found then the local Devise authentication will fail and eventually the user is redirected to the 'user/saml/sign_in page'.  In the current version of the gem this results in an error due to missing data when creating the SAML response.  Additionally, the user will not know why they were given an error, even though they signed in successfully to the IdP. 

    2. To fix this problem Devise failure mechanism must be modified to redirect the user to the landing page or another page of your choice.
    3. Create the following custom failure class in the lib directory.  Modify the redirect_url method to use any other page and message. 

      /lib/auth_failure.rb
      class AuthFailure < Devise::FailureApp
      
        def redirect_url 
          flash[:alert] = I18n.translate('auth_failure') 				#display message from locales/en.yml
          Figaro.env.slo_success_url									#redirect to the landing page
        end 
        def respond
          if http_auth?
            http_auth
          else
            redirect
          end
        end  
      end
    4. Add the loader to application.rb

      config/application.rb
      config.autoload_paths << "#{Rails.root}/lib"
    5. Add the failure app location in the devise initializer

      config/initializers/devise.rb
        config.warden do |manager|
          manager.failure_app = AuthFailure
        end
  9. Updating Attributes after successful sign in (optional)
    1. The attribute mapping file is used to update the local database with data send from the IdP.  The default method was not functional on testing but can be modified in the devise config file. The following code updates the user, but skips the validations by using the update_columns method.

      config/initializers/devise.rb
          config.saml_update_resource_hook = Proc.new do |user, saml_response, auth_value|
           attrs = saml_response.attributes.resource_keys.map{ |key| [key, saml_response.attribute_value_by_resource_key(key)] }.to_h
           user.update_columns(attrs)
          end
  10. Automated Testing with cucumber
    1. The selenium driver must be used because of course capybara/cucumber cannot interact with the remote server in the normal way
    2. Testing revealed that the 'truncation' database strategy worked for testing sign in 
    3. I setup a test IdP app using https://github.com/sportngin/saml_idp to test the sign in process and then tested against the staging IdP