Skip to content

Instantly share code, notes, and snippets.

@heridev
Created October 13, 2021 00:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save heridev/ec5208a297a143d7e4c3bec14b49e406 to your computer and use it in GitHub Desktop.
Save heridev/ec5208a297a143d7e4c3bec14b49e406 to your computer and use it in GitHub Desktop.
Third party SAML Single Sign On (SSO) integration/implementation - Tech spec

Third party SAML Single Sign On (SSO) integration/implementation - Tech spec

  • Author: Heriberto Perez

Background

The implementation of SSO in order to connect with other services/providers/sites is a common requirement these days

For those cases when you have the need to integrate a third party service and embed some widgets in your site, and in order to make it in a secure way and based on dynamic data for the current authenticated, that is when the SSO integration comes handy for you.

Goals

This Tech spec will serve as a reference a SAML Single Sign On (SSO) integration. The main goal here is to architect and implement the SSO integration with a third party service, in this sign-in flow we act as the Identify Provider(IdP) and the third party service as the Service Provider (SP) and the flow is initiated in the SP side, more about this in the upcoming sections and diagrams.

In addition to that, we should be able to test our integration locally, either by automated tests or by building our own dummy Service Provider (Manual testing).

Context and architecture

First of all, What is SAML?

SAML is a standard for security, specifically, for building single sign-on systems. It originated in 2002. Like almost all modern security concepts, SAML is oriented around roles. There are three key roles: Principal, Identity Provider, Service Provider.

Roles

  • The principal is very, very simple – it is just the user.
  • The Identity Provider or IdP: The service in which we want to log in, generally with a user and a password, in this case, it is us.
  • The Service provider(SP) is the software that talks to the IdP that requests and obtains an identity assertion, in this case, it is the third party service.

There are two important sign-in flows for which authentication can be handled by SAML:

SP-initiated flow

When the user attempts to sign onto a SAML-enabled SP via its login page. Instead of prompting the user to enter the credentials, an SP that has been configured to use SAML will redirect the user to the IdP (Google, Facebook, etc). In our case, We will then handle the authentication either by using an email/password. If the user’s credentials are correct and the user has been granted access to the application on our site, they will be redirected back to the Service Provider as a verified user.

IdP-initiated flow

When the user logs into the IdP (Google, Facebook, etc) and launches the SP application by clicking its icon/button/option somewhere. If the user has an account on the SP side, they will be authenticated as a user of the application and will generally be delivered to its default landing page (their actual destination within the SP's site can be customizable depending on the SP). If they do NOT currently have an account on the SP side, in some cases SAML can actually create the user's account immediately in a process known as Just In Time Provisioning (JIT).

A couple of key things to note:

  1. The Service Provider never directly interacts with the Identity Provider. A browser acts as the agent to carry out all the redirections.
  2. The Service Provider needs to know which Identity Provider to redirect to before it has any idea who the user is.
  3. The Service Provider does not know who the user is until the SAML assertion comes back from the Identity Provider.
  4. This flow does not have to start with the Service Provider. An Identity Provider can initiate an authentication flow.
  5. The SAML authentication flow is asynchronous. The Service Provider does not know if the Identity Provider will ever complete the entire flow. Because of this, the Service Provider does not maintain any state of any authentication requests generated. When the Service Provider receives a response from an Identity Provider, the response must contain all the necessary information.

So in our case, we need to implement the functionality for our Rails App in order for us to act as a SAML Identity Provider (IDP).

Diagram about the Sign-in flow image

https://docs.google.com/presentation/d/1fYA9rZUZqJEu6_csd5ymZ1mTjQtT6eJcGvysh7AS-hY/edit#slide=id.g74e73bd42b_0_198

After that being said, here is some of the code implemented for the IdP:

  1. First, we would need to install this gem: https://github.com/sportngin/saml_idp
gem 'saml_idp'
  1. Generate new certificates by running the following command (It uses a passphrase):
openssl req -x509 -sha256 -nodes -days 3650 -passout pass:foobar -newkey rsa:2048 -keyout newCertWithPasswordLocahostKey.key -out newCertWithPasswordLocahostCert.crt

As you can see we are using the passphrase is foobar in the:

pass:foobar
  1. Let's include those certificates in the serializer:
# config/initializers/saml_idp.rb
SamlIdp.configure do |config|
  config.x509_certificate = <<-CERT
  -----BEGIN CERTIFICATE-----
fKWNq81HZDwg6JgqMHMGA/mPyLMA0GCSMBLzpkGUJPxtzIPEtqPLrK5oO5zDtTjV
fKWNq81HZDwg6JgqMHMGA/mPyLMA0GCSMBLzpkGUJPxtzIPEtqPLrK5oO5zDtTjV
...
fKWNq81HZDwg6JgqMHMGA/mPyLMA0GCSMBLzpkGUJPxtzIPE=
-----END CERTIFICATE-----
  CERT
  # for production environments we will use some ENV variables
  # for both secret and public certificate
  config.secret_key = ENV["SAML_SECRET_KEY"]
end
  1. Create new routes:

config/routes.rb

# Identify Provider - saml IDP
get '/idp/saml/auth' => 'saml_idp#new'

Once we implemented the routes we need to create some controllers for that, this is an example about how will need to implement some logic for the create method and the controller app/controllers/saml_idp_controller.rb:

class SamlIdpController < SamlIdp::IdpController
  before_action :authenticate_user!

  def new
    if current_user.blank?
      @html_content = '<h1>You need to log-in first</h1>'
    else
      http_response = make_http_to_get_resources_from_sp
      if http_response.code == '200'
        @html_content = http_response.body
      else
        @html_content = '<h1>There was an error when trying to render this resource, please reload this page</h1>'
      end
    end

    respond_to do |format|
      format.html { render :new, layout: 'widgets' }
    end
  end

  def make_http_to_get_resources_from_sp
    saml_response = idp_make_saml_response
    data = {
      SAMLResponse: saml_response,
      # this one was received from the Service
      # Provider just in case we need to render
      # a dynamic section/widget
      page_name: params[:page_name]
    }

    make_http_request(saml_acs_url, data)
  end

  private

  def make_http_request(url, data)
    ::Services::Request.make(url, 'Post', {}, data)
  end

  def idp_make_saml_response
    encryption_values = {
      cert: SamlIdp.config.x509_certificate,
      block_encryption: 'aes256-cbc',
      key_transport: 'rsa-oaep-mgf1p'
    }
    encode_response current_user, encryption_values
  end

  def idp_logout
    puts '============= IPD user logout needs to be implemented ============================'
  end
end

And the initializer looks like this:

SamlIdp.configure do |config|
  base = 'http://localhost:3030'
  config.x509_certificate = ENV['SAML_IDP_X509_CERTIFICATE']
  config.secret_key = ENV['SAML_IDP_SECRET_KEY']
  config.password = ENV['SAML_CERTIFICATE_PASSPHRASE']

  config.name_id.formats = {
      persistent: -> (principal) do
        User.find_by(id: principal.id).id
      end
    }

  config.attributes = {
    'Email address' => {
      'name' => 'email',
      'name_format' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
      'getter' => ->(principal) {
        principal.email
      },
    },
    'Name' => {
      'name' => 'name',
      'name_format' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
      'getter' => ->(principal) {
        principal.name
      }
    },
    'Role name' => {
      'name' => 'role',
      'name_format' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
      'getter' => ->(principal) {
        principal.role
      }
    }
  }

 # `identifier` is the entity_id or issuer of the Service Provider,
  # settings is an IncomingMetadata object which has a to_h method that needs to be persisted
  config.service_provider.metadata_persister = ->(identifier, settings) {
    fname = identifier.to_s.gsub(/\/|:/,'_')
    `mkdir -p #{Rails.root.join('cache/saml/metadata')}`
    File.open Rails.root.join('cache/saml/metadata/#{fname}'), 'r+b' do |f|
      Marshal.dump settings.to_h, f
    end
  }

  # `identifier` is the entity_id or issuer of the Service Provider,
  # `service_provider` is a ServiceProvider object. Based on the `identifier` or the
  # `service_provider` you should return the settings.to_h from above
  config.service_provider.persisted_metadata_getter = ->(identifier, service_provider){
    fname = identifier.to_s.gsub(/\/|:/,'_')
    `mkdir -p #{Rails.root.join('cache/saml/metadata')}`
    full_filename = Rails.root.join('cache/saml/metadata/#{fname}')
    if File.file?(full_filename)
      File.open full_filename, 'rb' do |f|
        Marshal.load f
      end
    end
  }

  sp_metadata_url = ENV['PROVIDER_METADATA_URL'] || 'https://localhost:3030/sp/saml/metadata'
  idp_cert_fingerprint = ENV['IDP_CERT_FINGERPRINT'] || 'E4:FC:60:40:03:A2:33:9D:AA:9D:50:59:F2:04:F0:3C:88:62:3B:F1:EB:D8:4C:FF:9C:D1:93:07:03:F7:C9:74'
  sp_saml_auth = ENV['SP_SSO_AUTH_TARGET_URL'] || 'https://localhost:3030/sp/saml/auth'

  service_provider_list = {
    sp_metadata_url => {
      fingerprint: idp_cert_fingerprint,
      metadata_url: sp_metadata_url,
      response_hosts: [sp_saml_auth]
    }
  }

  # Find ServiceProvider metadata_url and fingerprint based on our settings
  config.service_provider.finder = ->(issuer_or_entity_id) do
    service_provider_list[issuer_or_entity_id]
  end
end

That's it for the Identity Provider, but maybe now you would ask the question: How do I test this new endpoint? and for that, you can take a look at this ready to use SAML Service Provider: https://github.com/heridev/saml_service_provider_in_rails

In addition to that, you can play with two independent repositories in the next section :)

Standalone Service Provider and Identity Provider in Rails 6

I created two new repositories using Rails 6, so you can play with them without the need to make changes to your app

Service Provider Repository https://github.com/heridev/saml_service_provider_in_rails

Identity Provider Repository https://github.com/heridev/saml_identity_provider_rails

Other resources / documentation

  1. Get the fingerprint of a certificate:
openssl x509 -text -noout -in ~/Downloads/<your file name> -fingerprint -sha256
  1. One of the tools to generate our IDP metadata is going to be used here:
https://www.samltool.com/idp_metadata.php
  1. Great Wiki using the saml_idp gem as we are going to use it as well https://github.com/saml-idp/saml_idp/wiki

  2. Creating a Service Provider using the devise_saml_authenticatable gem https://qiita.com/alokrawat050/items/98a40c414d06a6e679ca

  3. Another way of creating a Service Provider with the ruby-saml gem: https://github.com/onelogin/ruby-saml

  4. Another way of implementing the Service Provider with the omniauth-saml gem (this one looks simpler than the others using the device and ruby-saml gems) and it shows how to allow using multiple IDPs. https://madeintandem.com/blog/configuring-rails-app-single-sign-saml-multiple-providers/

  5. Explanation about sign-in flows and some concepts about the SAML SP and IdP. https://duo.com/blog/the-beer-drinkers-guide-to-saml

  6. Testing assertions https://sptest.iamshowcase.com/.

  7. This is also a really good guide to implement the SAML and there are some key concepts included in here that are not mentioned in this tech spec (By Okta) https://developer.okta.com/docs/concepts/saml/#planning-for-saml

That's it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment