- Author: Heriberto Perez
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.
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).
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.
- 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:
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.
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:
- The
Service Provider
never directly interacts with the Identity Provider. A browser acts as the agent to carry out all the redirections. - The
Service Provider
needs to know which Identity Provider to redirect to before it has any idea who the user is. - The
Service Provider
does not know who the user is until the SAML assertion comes back from the Identity Provider. - This flow does not have to start with the Service Provider. An Identity Provider can initiate an authentication flow.
- 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
After that being said, here is some of the code implemented for the IdP:
- First, we would need to install this gem: https://github.com/sportngin/saml_idp
gem 'saml_idp'
- 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
- 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
- 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 :)
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
- Get the fingerprint of a certificate:
openssl x509 -text -noout -in ~/Downloads/<your file name> -fingerprint -sha256
- One of the tools to generate our IDP metadata is going to be used here:
https://www.samltool.com/idp_metadata.php
-
Great Wiki using the
saml_idp
gem as we are going to use it as well https://github.com/saml-idp/saml_idp/wiki -
Creating a Service Provider using the
devise_saml_authenticatable
gem https://qiita.com/alokrawat050/items/98a40c414d06a6e679ca -
Another way of creating a Service Provider with the
ruby-saml
gem: https://github.com/onelogin/ruby-saml -
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/ -
Explanation about sign-in flows and some concepts about the SAML SP and IdP. https://duo.com/blog/the-beer-drinkers-guide-to-saml
-
Testing assertions https://sptest.iamshowcase.com/.
-
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.