Version 1.3.0 is a recommended update for all Ruby SAML users as it includes security fixes. It adds security improvements in order to prevent Signature wrapping attacks. CVE-2016-5697
Version 1.2 adds IDP metadata parsing improvements, uuid deprecation in favour of SecureRandom, refactor error handling and some minor improvements
There is no compatibility issue detected.
For more details, please review the changelog.
Version 1.1 adds some improvements on signature validation and solves some namespace conflicts.
Version 1.0 is a recommended update for all Ruby SAML users as it includes security fixes.
Version 1.0 adds security improvements like entity expansion limitation, more SAML message validations, and other important improvements like decrypt support.
Please note the get_idp_metadata method raises an exception when it is not able to fetch the idp metadata, so review your integration if you are using this functionality.
Version 0.9 adds many new features and improvements.
Version 0.8.x changes the namespace of the gem from OneLogin::Saml to OneLogin::RubySaml. Please update your implementations of the gem accordingly.
The Ruby SAML library is for implementing the client side of a SAML authorization, i.e. it provides a means for managing authorization initialization and confirmation requests from identity providers.
SAML authorization is a two step process and you are expected to implement support for both.
We created a demo project for Rails4 that uses the latest version of this library: ruby-saml-example
- 1.8.7
- 1.9.x
- 2.0.x
- 2.1.x
- 2.2.x
- JRuby 1.7.19
- JRuby 9.0.0.0
- Fork the repository
- Make your feature addition or bug fix
- Add tests for your new features. This is important so we don't break any features in a future version unintentionally.
- Ensure all tests pass.
- Do not change rakefile, version, or history.
- Open a pull request, following this template.
In order to use the toolkit you will need to install the gem (either manually or using Bundler), and require the library in your Ruby application:
Using Gemfile
# latest stablegem'ruby-saml','~> 1.0.0'# or track master for bleeding-edgegem'ruby-saml',:github=>'onelogin/ruby-saml'Using RubyGems
gem install ruby-samlWhen requiring the gem, you can add the whole toolkit
require'onelogin/ruby-saml'or just the required components individually:
require'onelogin/ruby-saml/authrequest'This gem has a dependency on Nokogiri, which dropped support for Ruby 1.8.x in Nokogiri 1.6. When installing this gem on Ruby 1.8.7, you will need to make sure a version of Nokogiri prior to 1.6 is installed or specified if it hasn't been already.
Using Gemfile
gem'nokogiri','~> 1.5.10'Using RubyGems
gem install nokogiri --version '~> 1.5.10'When troubleshooting SAML integration issues, you will find it extremely helpful to examine the output of this gem's business logic. By default, log messages are emitted to RAILS_DEFAULT_LOGGER when the gem is used in a Rails context, and to STDOUT when the gem is used outside of Rails.
To override the default behavior and control the destination of log messages, provide a ruby Logger object to the gem's logging singleton:
OneLogin::RubySaml::Logging.logger=Logger.new(File.open('/var/log/ruby-saml.log','w'))This is the first request you will get from the identity provider. It will hit your application at a specific URL (that you've announced as being your SAML initialization point). The response to this initialization, is a redirect back to the identity provider, which can look something like this (ignore the saml_settings method call for now):
definitrequest=OneLogin::RubySaml::Authrequest.newredirect_to(request.create(saml_settings))endOnce you've redirected back to the identity provider, it will ensure that the user has been authorized and redirect back to your application for final consumption, this is can look something like this (the authorize_success and authorize_failure methods are specific to your application):
defconsumeresponse=OneLogin::RubySaml::Response.new(params[:SAMLResponse],:settings=>saml_settings)# We validate the SAML Response and check if the user already exists in the systemifresponse.is_valid?# authorize_success, log the usersession[:userid]=response.nameidsession[:attributes]=response.attributeselseauthorize_failure# This method shows an error messageendendIn the above there are a few assumptions in place, one being that the response.nameid is an email address. This is all handled with how you specify the settings that are in play via the saml_settings method. That could be implemented along the lines of this:
If the assertion of the SAMLResponse is not encrypted, you can initialize the Response without the :settings parameter and set it later,
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings
but if the SAMLResponse contains an encrypted assertion, you need to provide the settings in the initialize method in order to be able to obtain the decrypted assertion, using the service provider private key in order to decrypt. If you don't know what expect, use always the first proposed way (always set the settings on the initialize method).
defsaml_settingssettings=OneLogin::RubySaml::Settings.newsettings.assertion_consumer_service_url="http://#{request.host}/saml/consume"settings.issuer="http://#{request.host}/saml/metadata"settings.idp_sso_target_url="https://app.onelogin.com/saml/metadata/#{OneLoginAppId}"settings.idp_entity_id="https://app.onelogin.com/saml/metadata/#{OneLoginAppId}"settings.idp_sso_target_url="https://app.onelogin.com/trust/saml2/http-post/sso/#{OneLoginAppId}"settings.idp_slo_target_url="https://app.onelogin.com/trust/saml2/http-redirect/slo/#{OneLoginAppId}"settings.idp_cert_fingerprint=OneLoginAppCertFingerPrintsettings.idp_cert_fingerprint_algorithm="http://www.w3.org/2000/09/xmldsig#sha1"settings.name_identifier_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"# Optional for most SAML IdPssettings.authn_context="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"# Optional bindings (defaults to Redirect for logout POST for acs)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"settingsendSome assertion validations can be skipped by passing parameters to OneLogin::RubySaml::Response.new(). For example, you can skip the Conditions validation or the SubjectConfirmation validations by initializing the response with different options:
response=OneLogin::RubySaml::Response.new(params[:SAMLResponse],{skip_conditions: true})# skips conditionsresponse=OneLogin::RubySaml::Response.new(params[:SAMLResponse],{skip_subject_confirmation: true})# skips subject confirmationWhat's left at this point, is to wrap it all up in a controller and point the initialization and consumption URLs in OneLogin at that. A full controller example could look like this:
# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.classSamlController < ApplicationControllerdefinitrequest=OneLogin::RubySaml::Authrequest.newredirect_to(request.create(saml_settings))enddefconsumeresponse=OneLogin::RubySaml::Response.new(params[:SAMLResponse])response.settings=saml_settings# We validate the SAML Response and check if the user already exists in the systemifresponse.is_valid?# authorize_success, log the usersession[:userid]=response.nameidsession[:attributes]=response.attributeselseauthorize_failure# This method shows an error messageendendprivatedefsaml_settingssettings=OneLogin::RubySaml::Settings.newsettings.assertion_consumer_service_url="http://#{request.host}/saml/consume"settings.issuer="http://#{request.host}/saml/metadata"settings.idp_sso_target_url="https://app.onelogin.com/saml/signon/#{OneLoginAppId}"settings.idp_cert_fingerprint=OneLoginAppCertFingerPrintsettings.name_identifier_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"# Optional for most SAML IdPssettings.authn_context="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"# Optional. Describe according to IdP specification (if supported) which attributes the SP desires to receive in SAMLResponse.settings.attributes_index=5# Optional. Describe an attribute consuming service for support of additional attributes.settings.attribute_consuming_service.configuredoservice_name"Service"service_index5add_attribute:name=>"Name",:name_format=>"Name Format",:friendly_name=>"Friendly Name"endsettingsendendThe method above requires a little extra work to manually specify attributes about the IdP. (And your SP application) There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata.
Using idp_metadata_parser.parse_remote IdP metadata will be added to the settings withouth further ado.
defsaml_settingsidp_metadata_parser=OneLogin::RubySaml::IdpMetadataParser.new# Returns OneLogin::RubySaml::Settings prepopulated with idp metadatasettings=idp_metadata_parser.parse_remote("https://example.com/auth/saml2/idp/metadata")settings.assertion_consumer_service_url="http://#{request.host}/saml/consume"settings.issuer="http://#{request.host}/saml/metadata"settings.name_identifier_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"# Optional for most SAML IdPssettings.authn_context="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"settingsendThe following attributes are set:
- idp_sso_target_url
- idp_slo_target_url
- idp_cert_fingerprint
If you are using saml:AttributeStatement to transfer metadata, like the user name, you can access all the attributes through response.attributes. It contains all the saml:AttributeStatement with its 'Name' as a indifferent key the one/more saml:AttributeValue as value. The value returned depends on the value of the
single_value_compatibility (when activate, only one value returned, the first one)
response=OneLogin::RubySaml::Response.new(params[:SAMLResponse])response.settings=saml_settingsresponse.attributes[:username]Imagine this saml:AttributeStatement
<saml:AttributeStatement>
<saml:AttributeName="uid">
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:type="xs:string">demo</saml:AttributeValue>
</saml:Attribute>
<saml:AttributeName="another_value">
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:type="xs:string">value1</saml:AttributeValue>
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:type="xs:string">value2</saml:AttributeValue>
</saml:Attribute>
<saml:AttributeName="role">
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:type="xs:string">role1</saml:AttributeValue>
</saml:Attribute>
<saml:AttributeName="role">
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:type="xs:string">role2</saml:AttributeValue>
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:type="xs:string">role3</saml:AttributeValue>
</saml:Attribute>
<saml:AttributeName="attribute_with_nil_value">
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:nil="true"/>
</saml:Attribute>
<saml:AttributeName="attribute_with_nils_and_empty_strings">
<saml:AttributeValue/>
<saml:AttributeValue>valuePresent</saml:AttributeValue>
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:nil="true"/>
<saml:AttributeValuexmlns:xs="http://www.w3.org/2001/XMLSchema"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:nil="1"/>
</saml:Attribute>
</saml:AttributeStatement>pp(response.attributes)# is an OneLogin::RubySaml::Attributes object# => @attributes={"uid"=>["demo"],"another_value"=>["value1","value2"],"role"=>["role1","role2","role3"],"attribute_with_nil_value"=>[nil],"attribute_with_nils_and_empty_strings"=>["","valuePresent",nil,nil]}>
# Active single_value_compatibilityOneLogin::RubySaml::Attributes.single_value_compatibility=truepp(response.attributes[:uid])# => "demo"pp(response.attributes[:role])# => "role1"pp(response.attributes.single(:role))# => "role1"pp(response.attributes.multi(:role))# => ["role1", "role2", "role3"]pp(response.attributes[:attribute_with_nil_value])# => nilpp(response.attributes[:attribute_with_nils_and_empty_strings])# => ""pp(response.attributes[:not_exists])# => nilpp(response.attributes.single(:not_exists))# => nilpp(response.attributes.multi(:not_exists))# => nil# Deactive single_value_compatibilityOneLogin::RubySaml::Attributes.single_value_compatibility=falsepp(response.attributes[:uid])# => ["demo"]pp(response.attributes[:role])# => ["role1", "role2", "role3"]pp(response.attributes.single(:role))# => "role1"pp(response.attributes.multi(:role))# => ["role1", "role2", "role3"]pp(response.attributes[:attribute_with_nil_value])# => [nil]pp(response.attributes[:attribute_with_nils_and_empty_strings])# => ["", "valuePresent", nil, nil]pp(response.attributes[:not_exists])# => nilpp(response.attributes.single(:not_exists))# => nilpp(response.attributes.multi(:not_exists))# => nilThe saml:AuthnContextClassRef of the AuthNRequest can be provided by settings.authn_context , possible values are described at [SAMLAuthnCxt]. The comparison method can be set using the parameter settings.authn_context_comparison (the possible values are: 'exact', 'better', 'maximum' and 'minimum'), 'exact' is the default value.
If we want to add a saml:AuthnContextDeclRef, define a settings.authn_context_decl_ref.
The Ruby Toolkit supports 2 different kinds of signature: Embeded and as GET parameter
In order to be able to sign we need first to define the private key and the public cert of the service provider
settings.certificate="CERTIFICATE TEXT WITH HEAD AND FOOT"settings.private_key="PRIVATE KEY TEXT WITH HEAD AND FOOT"The settings related to sign are stored in the security attribute of the settings:
settings.security[:authn_requests_signed]=true# Enable or not signature on AuthNRequestsettings.security[:logout_requests_signed]=true# Enable or not signature on Logout Requestsettings.security[:logout_responses_signed]=true# Enable or not signatureonLogoutResponsesettings.security[:want_assertions_signed]=true# Enable or not therequirementofsignedassertionsettings.security[:metadata_signed]=true# Enable or not signature on Metadatasettings.security[:digest_method]=XMLSecurity::Document::SHA1settings.security[:signature_method]=XMLSecurity::Document::RSA_SHA1# Embeded signature or HTTP GET parameter signature# Note that metadata signature is always embedded regardless of this value.settings.security[:embed_sign]=falseNotice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding, remember to provide it to the Signature builder if you are sending a GET RelayState parameter or Signature validation process will fail at the Identity Provider.
The Service Provider will sign the request/responses with its private key. The Identity Provider will validate the sign of the received request/responses with the public x500 cert of the Service Provider.
Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and the decrypt process.
Enable/disable the soft mode by the settings.soft parameter. When is set false, the saml validations errors will raise an exception.
The Ruby Toolkit supports EncryptedAssertion.
In order to be able to decrypt a SAML Response that contains a EncryptedAssertion we need first to define the private key and the public cert of the service provider, and share this with the Identity Provider.
settings.certificate="CERTIFICATE TEXT WITH HEAD AND FOOT"settings.private_key="PRIVATE KEY TEXT WITH HEAD AND FOOT"The Identity Provider will encrypt the Assertion with the public cert of the Service Provider. The Service Provider will decrypt the EncryptedAssertion with its private key.
Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and the decrypt process.
The Ruby Toolkit supports SP-initiated Single Logout and IdP-Initiated Single Logout.
Here is an example that we could add to our previous controller to generate and send a SAML Logout Request to the IdP
# Create a SP initiated SLOdefsp_logout_request# LogoutRequest accepts plain browser requests w/o paramterssettings=saml_settingsifsettings.idp_slo_target_url.nil?logger.info"SLO IdP Endpoint not found in settings, executing then a normal logout'"delete_sessionelse# Since we created a new SAML request, save the transaction_id# to compare it with the response we get backlogout_request=OneLogin::RubySaml::Logoutrequest.new()session[:transaction_id]=logout_request.uuidlogger.info"New SP SLO for userid '#{session[:userid]}' transactionid '#{session[:transaction_id]}'"ifsettings.name_identifier_value.nil?settings.name_identifier_value=session[:userid]endrelayState=url_forcontroller: 'saml',action: 'index'redirect_to(logout_request.create(settings,:RelayState=>relayState))endendand this method process the SAML Logout Response sent by the IdP as reply of the SAML Logout Request
# After sending an SP initiated LogoutRequest to the IdP, we need to accept# the LogoutResponse, verify it, then actually delete our session.defprocess_logout_responsesettings=Account.get_saml_settingsifsession.has_key?:transaction_idlogout_response=OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse],settings,:matches_request_id=>session[:transaction_id])elselogout_response=OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse],settings)endlogger.info"LogoutResponse is: #{logout_response.to_s}"# Validate the SAML Logout Responseif not logout_response.validatelogger.error"The SAML Logout Response is invalid"else# Actually log out this sessioniflogout_response.success?logger.info"Delete session for '#{session[:userid]}'"delete_sessionendendend# Delete a user's session.defdelete_sessionsession[:userid]=nilsession[:attributes]=nilendHere is an example that we could add to our previous controller to process a SAML Logout Request from the IdP and reply a SAML Logout Response to the IdP
# Method to handle IdP initiated logoutsdefidp_logout_requestsettings=Account.get_saml_settingslogout_request=OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest])if !logout_request.is_valid?logger.error"IdP initiated LogoutRequest was not valid!"render:inline=>logger.errorendlogger.info"IdP initiated Logout for #{logout_request.name_id}"# Actually log out this sessiondelete_session# Generate a response to the IdP.logout_request_id=logout_request.idlogout_response=OneLogin::RubySaml::SloLogoutresponse.new.create(settings,logout_request_id,nil,:RelayState=>params[:RelayState])redirect_tologout_responseendAll the mentioned methods could be handled in a unique view:
# Trigger SP and IdP initiated Logout requestsdeflogout# If we're given a logout request, handle it in the IdP logout initiated methodifparams[:SAMLRequest]returnidp_logout_request# We've been given a response back from the IdP, process itelsifparams[:SAMLResponse]returnprocess_logout_response# Initiate SLO (send Logout Request)elsereturnsp_logout_requestendendTo form a trusted pair relationship with the IdP, the SP (you) need to provide metadata XML to the IdP for various good reasons. (Caching, certificate lookups, relaying party permissions, etc)
The class OneLogin::RubySaml::Metadata takes care of this by reading the Settings and returning XML. All you have to do is add a controller to return the data, then give this URL to the IdP administrator.
The metdata will be polled by the IdP every few minutes, so updating your settings should propagate to the IdP settings.
classSamlController < ApplicationController# ... the rest of your controller definitions ...defmetadatasettings=Account.get_saml_settingsmeta=OneLogin::RubySaml::Metadata.newrender:xml=>meta.generate(settings),:content_type=>"application/samlmetadata+xml"endendServer clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition" then this may be due to clock differences between your system and that of the Identity Provider.
First, ensure that both systems synchronize their clocks, using for example the industry standard Network Time Protocol (NTP).
Even then you may experience intermittent issues though, because the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift you can initialize the response passing in an option named :allowed_clock_drift. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the NotBefore assertion. For example:
response=OneLogin::RubySaml::Response.new(params[:SAMLResponse],:allowed_clock_drift=>1.second)Make sure to keep the value as comfortably small as possible to keep security risks to a minimum.
To request attributes from the IdP the SP needs to provide an attribute service within it's metadata and reference the index in the assertion.
settings=OneLogin::RubySaml::Settings.newsettings.attributes_index=5settings.attribute_consuming_service.configuredoservice_name"Service"service_index5add_attribute:name=>"Name",:name_format=>"Name Format",:friendly_name=>"Friendly Name"add_attribute:name=>"Another Attribute",:name_format=>"Name Format",:friendly_name=>"Friendly Name",:attribute_value=>"Attribute Value"end