diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0ef9bf..519399bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [#692](https://github.com/SAML-Toolkits/ruby-saml/pull/692) Remove `XMLSecurity` namespace and replace with `RubySaml::XML`. * [#686](https://github.com/SAML-Toolkits/ruby-saml/pull/686) Use SHA-256 as the default hashing algorithm everywhere instead of SHA-1, including signatures, fingerprints, and digests. * [#695](https://github.com/SAML-Toolkits/ruby-saml/pull/695) Deprecate `settings.compress_request` and `settings.compess_response` parameters. +* [#701](https://github.com/SAML-Toolkits/ruby-saml/pull/695) Deprecate `settings.idp_cert_fingerprint` and `settings.idp_cert_fingerprint_algorithm` parameters. * [#690](https://github.com/SAML-Toolkits/ruby-saml/pull/690) Remove deprecated `settings.security[:embed_sign]` parameter. * [#697](https://github.com/SAML-Toolkits/ruby-saml/pull/697) Add deprecation for various parameters in `RubySaml::Settings`. * [#709](https://github.com/SAML-Toolkits/ruby-saml/pull/709) Allow passing in `Net::HTTP` `:open_timeout`, `:read_timeout`, and `:max_retries` settings to `IdpMetadataParser#parse_remote`. diff --git a/README.md b/README.md index 5ed96767..92dcb152 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,41 @@ [![Rubygem Version](https://badge.fury.io/rb/ruby-saml.svg)](https://badge.fury.io/rb/ruby-saml) [![GitHub version](https://badge.fury.io/gh/SAML-Toolkits%2Fruby-saml.svg)](https://badge.fury.io/gh/SAML-Toolkits%2Fruby-saml) ![GitHub](https://img.shields.io/github/license/SAML-Toolkits/ruby-saml) ![Gem](https://img.shields.io/gem/dtv/ruby-saml?label=gem%20downloads%20latest) ![Gem](https://img.shields.io/gem/dt/ruby-saml?label=gem%20total%20downloads) -Ruby SAML minor and tiny versions may introduce breaking changes. Please read +Ruby SAML minor versions may introduce breaking changes. Please read [UPGRADING.md](UPGRADING.md) for guidance on upgrading to new Ruby SAML versions. +## Vulnerability Notice + **There is a critical vulnerability affecting ruby-saml < 1.17.0 (CVE-2024-45409). Make sure you are using an updated version. (1.12.3 is safe)** ## Overview -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. +The Ruby SAML library is used by Service Providers (SPs) to implement SAML authentication. +It enables SPs to create SAML AuthnRequests (authentication requests) and validate SAML +Response assertions from Identity Providers (IdPs). + +**Important:** This libary does not support the IdP-side of SAML authentication, +such as creating SAML Response messages to assert a user's identity. + +A Rails 4 reference implemenation is avaiable at the +[Ruby SAML Demo Project](https://github.com/saml-toolkits/ruby-saml-example). + +### Vulnerability Reporting -SAML authorization is a two step process and you are expected to implement support for both. +If you believe you have discovered a security vulnerability in this gem, please report +it by email to the maintainer: sixto.martin.garcia+security@gmail.com -We created a demo project for Rails 4 that uses the latest version of this library: -[ruby-saml-example](https://github.com/saml-toolkits/ruby-saml-example) +### Security Considerations + +- **Validation of the IdP Metadata URL:** When loading IdP Metadata from a URLs, + Ruby SAML requires you (the developer/administrator) to ensure the supplied URL is correct + and from a trusted source. Ruby SAML does not perform any validation that the URL + you entered is correct and/or safe. +- **False-Positive Security Warnings:** Some tools may incorrectly report Ruby SAML as a + potential security vulnerability, due to it's dependency on Nokogiri. Such warnings can + be ignored; Ruby SAML uses Nokogiri in a safe way, by always disabling its DTDLOAD option + and enabling its NONET option. ### Supported Ruby Versions @@ -31,104 +50,32 @@ The following Ruby versions are covered by CI testing: Older Ruby versions are supported on the 1.x release of Ruby SAML. -## Adding Features, Pull Requests - -* 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 by running `bundle exec rake test`. -* Do not change rakefile, version, or history. -* Open a pull request, following [this template](https://gist.github.com/Lordnibbler/11002759). - -## Security Guidelines - -If you believe you have discovered a security vulnerability in this gem, please report it -by mail to the maintainer: sixto.martin.garcia+security@gmail.com - -### Security Warning - -Some tools may incorrectly report ruby-saml is a potential security vulnerability. -ruby-saml depends on Nokogiri, and it's possible to use Nokogiri in a dangerous way -(by enabling its DTDLOAD option and disabling its NONET option). -This dangerous Nokogiri configuration, which is sometimes used by other components, -can create an XML External Entity (XXE) vulnerability if the XML data is not trusted. -However, ruby-saml never enables this dangerous Nokogiri configuration; -ruby-saml never enables DTDLOAD, and it never disables NONET. - -The RubySaml::IdpMetadataParser class does not validate in any way the URL -that is introduced in order to be parsed. - -Usually the same administrator that handles the Service Provider also sets the URL to -the IdP, which should be a trusted resource. - -But there are other scenarios, like a SAAS app where the administrator of the app -delegates this functionality to other users. In this case, extra precaution should -be taken in order to validate such URL inputs and avoid attacks like SSRF. - ## Getting Started -In order to use Ruby SAML you will need to install the gem (either manually or using Bundler), -and require the library in your Ruby application: - -Using `Gemfile` - -```ruby -# latest stable -gem 'ruby-saml', '~> 1.11.0' - -# or track master for bleeding-edge -gem 'ruby-saml', :github => 'saml-toolkit/ruby-saml' -``` - -Using RubyGems +You may install Ruby SAML from the command line: ```sh gem install ruby-saml ``` -You may require the entire Ruby SAML gem: - -```ruby -require 'ruby_saml' -``` - -or just the required components individually: - -```ruby -require 'ruby_saml/authrequest' -``` - -### Installation on Ruby 1.8.7 - -This gem uses Nokogiri as a dependency, 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` +or in your project `Gemfile`: ```ruby -gem 'nokogiri', '~> 1.5.10' +gem 'ruby-saml', '~> 2.0.0' ``` -Using RubyGems - -```sh -gem install nokogiri --version '~> 1.5.10' -```` - ### Configuring Logging -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: +Ruby SAML provides verbose logs which are useful to troubleshooting SAML integration issues. +By default, log messages are emitted to Rails' logger if using Rails, otherwise to `STDOUT`. +You may manually set your own logger as follows: ```ruby RubySaml::Logging.logger = Logger.new('/var/log/ruby-saml.log') ``` +# Implementation Guide + ## The Initialization Phase This is the first request you will get from the identity provider. It will hit your application @@ -143,7 +90,8 @@ def init end ``` -If the SP knows who should be authenticated in the IdP, then can provide that info as follows: +If you (the SP) know which specific user should be authenticated by the IdP, +then can provide that info as follows: ```ruby def init @@ -155,22 +103,22 @@ end ``` Once 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 can look something like this (the `authorize_success` and `authorize_failure` +authenticated and redirect back to your application for final consumption. +This can look something like this (the `authn_success` and `authn_failure` methods are specific to your application): ```ruby def consume - response = RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings) + response = RubySaml::Response.new(params[:SAMLResponse], settings: saml_settings) # We validate the SAML Response and check if the user already exists in the system if response.is_valid? - # authorize_success, log the user - session[:userid] = response.nameid - session[:attributes] = response.attributes + authn_success(response) # This is your method to log the user, etc. + session[:userid] = response.nameid + session[:attributes] = response.attributes else - authorize_failure # This method shows an error message - # List of errors is available in response.errors array + authn_failure(response) # This is your method to log the failure and show an error message, etc. + # The list of errors is available in response.errors array end end ``` @@ -194,14 +142,14 @@ If you don't know what expect, always use the former (set the settings on initia def saml_settings settings = RubySaml::Settings.new - settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume" - settings.sp_entity_id = "http://#{request.host}/saml/metadata" - settings.idp_entity_id = "https://app.onelogin.com/saml/metadata/#{OneLoginAppId}" - settings.idp_sso_service_url = "https://app.onelogin.com/trust/saml2/http-post/sso/#{OneLoginAppId}" + settings.assertion_consumer_service_url = "https://www.my-domain.com/saml/consume" + settings.sp_entity_id = "https://www.my-domain.com/saml/metadata" + settings.idp_entity_id = "https://www.your-idp.com/saml/metadata/#{IdpAppId}" + settings.idp_sso_service_url = "https://www.your-idp.com/saml/#{IdpAppId}" settings.idp_sso_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" # or :post, :redirect - settings.idp_slo_service_url = "https://app.onelogin.com/trust/saml2/http-redirect/slo/#{OneLoginAppId}" + settings.idp_slo_service_url = "https://www.your-idp.com/saml/slo/#{IdpAppId}" settings.idp_slo_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" # or :post, :redirect - settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint + settings.idp_cert_fingerprint = IdpAppCertFingerPrint settings.idp_cert_fingerprint_algorithm = "http://www.w3.org/2000/09/xmldsig#sha256" settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" @@ -228,18 +176,18 @@ For example, you can skip the `AuthnStatement`, `Conditions`, `Recipient`, or th validations by initializing the response with different options: ```ruby -response = RubySaml::Response.new(params[:SAMLResponse], {skip_authnstatement: true}) # skips AuthnStatement -response = RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions -response = RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation -response = RubySaml::Response.new(params[:SAMLResponse], {skip_recipient_check: true}) # doesn't skip subject confirmation, but skips the recipient check which is a sub check of the subject_confirmation check -response = RubySaml::Response.new(params[:SAMLResponse], {skip_audience: true}) # skips audience check +response = RubySaml::Response.new(params[:SAMLResponse], { skip_authnstatement: true }) # skips AuthnStatement +response = RubySaml::Response.new(params[:SAMLResponse], { skip_conditions: true }) # skips conditions +response = RubySaml::Response.new(params[:SAMLResponse], { skip_subject_confirmation: true }) # skips subject confirmation +response = RubySaml::Response.new(params[:SAMLResponse], { skip_recipient_check: true }) # doesn't skip subject confirmation, but skips the recipient check which is a sub check of the subject_confirmation check +response = RubySaml::Response.new(params[:SAMLResponse], { skip_audience: true }) # skips audience check ``` All that's left is to wrap everything in a controller and reference it in the initialization and -consumption URLs in OneLogin. A full controller example could look like this: +consumption URLs. A full controller example could look like this: ```ruby -# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application. +# This controller expects you to use the URLs /saml/init and /saml/consume in your application. class SamlController < ApplicationController def init request = RubySaml::Authrequest.new @@ -252,12 +200,12 @@ class SamlController < ApplicationController # We validate the SAML Response and check if the user already exists in the system if response.is_valid? - # authorize_success, log the user - session[:userid] = response.nameid - session[:attributes] = response.attributes + authn_success(response) # This is your method to log the user, etc. + session[:userid] = response.nameid + session[:attributes] = response.attributes else - authorize_failure # This method shows an error message - # List of errors is available in response.errors array + authn_failure(response) # This is your method to log the failure and show an error message, etc. + # The list of errors is available in response.errors array end end @@ -266,10 +214,10 @@ class SamlController < ApplicationController def saml_settings settings = RubySaml::Settings.new - settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume" - settings.sp_entity_id = "http://#{request.host}/saml/metadata" - settings.idp_sso_service_url = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}" - settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint + settings.assertion_consumer_service_url = "https://www.my-sp-domain.com/saml/consume" + settings.sp_entity_id = "https://www.my-sp-domain.com/saml/metadata" + settings.idp_sso_service_url = "https://www.your-idp.com/saml/#{IdpAppId}" + settings.idp_cert_fingerprint = IdpAppCertFingerPrint settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" # Optional for most SAML IdPs @@ -281,7 +229,7 @@ class SamlController < ApplicationController settings.attribute_consuming_service.configure do service_name "Service" service_index 5 - add_attribute :name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name" + add_attribute name: "Name", name_format: "Name Format", friendly_name: "Friendly Name" end settings @@ -291,20 +239,22 @@ end ## Signature Validation -Ruby SAML allows different ways to validate the signature of the SAMLResponse: -- You can provide the IdP X.509 public certificate at the `idp_cert` setting. -- You can provide the IdP X.509 public certificate in fingerprint format using the - `idp_cert_fingerprint` setting parameter and additionally the `idp_cert_fingerprint_algorithm` parameter. +Ruby SAML allows different ways to validate the signature of the SAML Response: +- You may provide the IdP X.509 public certificate at the `idp_cert` setting. +- (Deprecated) You may provide the IdP X.509 public certificate in fingerprint format using the + `idp_cert_fingerprint` and `idp_cert_fingerprint_algorithm` parameters. -When validating the signature of redirect binding, the fingerprint is useless and the certificate -of the IdP is required in order to execute the validation. You can pass the option -`:relax_signature_validation` to `SloLogoutrequest` and `Logoutresponse` if want to avoid signature -validation if no certificate of the IdP is provided. +In addition, you may pass the option `:relax_signature_validation` to `SloLogoutrequest` and +`Logoutresponse` if want to skip signature validation on logout. -In production also we highly recommend to register on the settings the IdP certificate instead -of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision -attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, -we maintain it for compatibility and also to be used on test environment. +The `idp_cert_fingerprint` option is deprecated for the following reasons. It will be +removed in Ruby SAML version 2.1.0. +1. It only works with HTTP-POST binding, not HTTP-Redirect, since the full certificate + is not sent in the Redirect URL parameters. +2. It is theoretically be susceptible to collision attacks, by which a malicious + actor could impersonate the IdP. (However, as of January 2025, such attacks have not + been publicly demonstrated for SHA-256.) +3. It has been removed already from several other SAML libraries in other languages. ## Handling Multiple IdP Certificates @@ -320,8 +270,8 @@ add the IdP X.509 public certificates which were published in the IdP metadata. ```ruby { - :signing => [], - :encryption => [] + signing: [], + encryption: [] } ``` @@ -339,10 +289,10 @@ def saml_settings idp_metadata_parser = RubySaml::IdpMetadataParser.new # Returns RubySaml::Settings pre-populated with IdP metadata - settings = idp_metadata_parser.parse_remote("https://example.com/auth/saml2/idp/metadata") + settings = idp_metadata_parser.parse_remote("https://www.your-idp.com/saml/metadata/#{IdpAppId}.xml") - settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume" - settings.sp_entity_id = "http://#{request.host}/saml/metadata" + settings.assertion_consumer_service_url = "https://www.my-sp-domain.com/saml/consume" + settings.sp_entity_id = "https://www.my-sp-domain.com/saml/metadata" 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" @@ -403,7 +353,6 @@ The `RubySaml::IdpMetadataParser` also provides the methods `#parse_to_hash` and Those return an Hash instead of a `Settings` object, which may be useful for configuring [omniauth-saml](https://github.com/omniauth/omniauth-saml), for instance. - ### Validating Signature of Metadata and retrieve settings Right now there is no method at ruby_saml to validate the signature of the metadata that gonna be parsed, @@ -496,12 +445,12 @@ Imagine this `saml:AttributeStatement` ```ruby pp(response.attributes) # is an 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] - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"=>["usersName"]}> +# {"uid"=>["demo"], +# "another_value"=>["value1", "value2"], +# "role"=>["role1", "role2", "role3"], +# "attribute_with_nil_value"=>[nil], +# "attribute_with_nils_and_empty_strings"=>["", "valuePresent", nil, nil] +# "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"=>["usersName"]}> # Active single_value_compatibility RubySaml::Attributes.single_value_compatibility = true @@ -582,7 +531,7 @@ To add a `saml:AuthnContextDeclRef`, define `settings.authn_context_decl_ref`. In a SP-initiated flow, the SP can indicate to the IdP the subject that should be authenticated. This is done by defining the `settings.name_identifier_value_requested` before building the authrequest object. -## Service Provider Metadata +## SP Metadata To 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) @@ -598,7 +547,7 @@ class SamlController < ApplicationController def metadata settings = Account.get_saml_settings meta = RubySaml::Metadata.new - render :xml => meta.generate(settings), :content_type => "application/samlmetadata+xml" + render xml: meta.generate(settings), content_type: "application/samlmetadata+xml" end end ``` @@ -606,11 +555,11 @@ end You can add `ValidUntil` and `CacheDuration` to the SP Metadata XML using instead: ```ruby - # Valid until => 2 days from now - # Cache duration = 604800s = 1 week - valid_until = Time.now + 172800 - cache_duration = 604800 - meta.generate(settings, false, valid_until, cache_duration) +# Valid until => 2 days from now +# Cache duration = 604800s = 1 week +valid_until = Time.now + 172800 +cache_duration = 604800 +meta.generate(settings, false, valid_until, cache_duration) ``` ## Signing and Decryption @@ -625,8 +574,8 @@ Ruby SAML supports the following functionality: In order to use functions 1-3 above, you must first define your SP public certificate and private key: ```ruby - settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" - settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" ``` Note that the same certificate (and its associated private key) are used to perform @@ -636,8 +585,8 @@ to specify different certificates for each function. You may also globally set the SP signature and digest method, to be used in SP signing (functions 1 and 2 above): ```ruby - settings.security[:digest_method] = RubySaml::XML::Document::SHA1 - settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 +settings.security[:digest_method] = RubySaml::XML::Document::SHA1 +settings.security[:signature_method] = RubySaml::XML::Document::RSA_SHA1 ``` #### Signing SP Metadata @@ -645,52 +594,52 @@ You may also globally set the SP signature and digest method, to be used in SP s You may add a `` digital signature element to your SP Metadata XML using the following setting: ```ruby - settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" - settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" - settings.security[:metadata_signed] = true # Enable signature on Metadata +settings.security[:metadata_signed] = true # Enable signature on Metadata ``` #### Signing SP SAML Messages -Ruby SAML supports SAML request signing. The Service Provider will sign the -request/responses with its private key. The Identity Provider will then validate the signature -of the received request/responses with the public X.509 cert of the Service Provider. +Ruby SAML supports SAML request signing. You (the SP) will sign the +request/responses with your private key. The IdP will then validate the signature +of the received request/responses with the SP's public X.509 cert. To enable, please first set your certificate and private key. This will add `` to your SP Metadata XML, to be read by the IdP. ```ruby - settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" - settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" ``` Next, you may specify the specific SP SAML messages you would like to sign: ```ruby - settings.security[:authn_requests_signed] = true # Enable signature on AuthNRequest - settings.security[:logout_requests_signed] = true # Enable signature on Logout Request - settings.security[:logout_responses_signed] = true # Enable signature on Logout Response +settings.security[:authn_requests_signed] = true # Enable signature on AuthNRequest +settings.security[:logout_requests_signed] = true # Enable signature on Logout Request +settings.security[:logout_responses_signed] = true # Enable signature on Logout Response ``` Signatures will be handled automatically for both `HTTP-Redirect` and `HTTP-Redirect` Binding. Note 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 the -signature validation process will fail at the Identity Provider. +signature validation process will fail at the IdP. #### Decrypting IdP SAML Assertions -Ruby SAML supports EncryptedAssertion. 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. +Ruby SAML supports EncryptedAssertion. The IdP will encrypt the Assertion with the +public cert of the SP. The SP will decrypt the EncryptedAssertion with its private key. You may enable EncryptedAssertion as follows. This will add `` to your SP Metadata XML, to be read by the IdP. ```ruby - settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" - settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.certificate = "CERTIFICATE TEXT WITH BEGIN/END HEADER AND FOOTER" +settings.private_key = "PRIVATE KEY TEXT WITH BEGIN/END HEADER AND FOOTER" - settings.security[:want_assertions_encrypted] = true # Invalidate SAML messages without an EncryptedAssertion +settings.security[:want_assertions_encrypted] = true # Invalidate SAML messages without an EncryptedAssertion ``` #### Verifying Signature on IdP Assertions @@ -701,7 +650,7 @@ The signature will be checked against the `` ele present in the IdP's metadata. ```ruby - settings.security[:want_assertions_signed] = true # Require the IdP to sign its SAML Assertions +settings.security[:want_assertions_signed] = true # Require the IdP to sign its SAML Assertions ``` #### Certificate and Signature Validation @@ -709,15 +658,15 @@ present in the IdP's metadata. You may require SP and IdP certificates to be non-expired using the following settings: ```ruby - settings.security[:check_idp_cert_expiration] = true # Raise error if IdP X.509 cert is expired - settings.security[:check_sp_cert_expiration] = true # Raise error SP X.509 cert is expired +settings.security[:check_idp_cert_expiration] = true # Raise error if IdP X.509 cert is expired +settings.security[:check_sp_cert_expiration] = true # Raise error SP X.509 cert is expired ``` By default, Ruby SAML will raise a `RubySaml::ValidationError` if a signature or certificate validation fails. You may disable such exceptions using the `settings.security[:soft]` parameter. ```ruby - settings.security[:soft] = true # Do not raise error on failed signature/certificate validations +settings.security[:soft] = true # Do not raise error on failed signature/certificate validations ``` #### Advanced SP Certificate Usage & Key Rollover @@ -743,7 +692,7 @@ settings.sp_cert_multi = { } ``` -Certificate rotation is acheived by inserting new certificates at the bottom of each list, +Certificate rotation is achieved by inserting new certificates at the bottom of each list, and then removing the old certificates from the top of the list once your IdPs have migrated. A common practice is for apps to publish the current SP metadata at a URL endpoint and have the IdP regularly poll for updates. @@ -824,7 +773,7 @@ def sp_logout_request session[:logged_out_user] = logged_user relayState = url_for(controller: 'saml', action: 'index') - redirect_to(logout_request.create(settings, :RelayState => relayState)) + redirect_to(logout_request.create(settings, 'RelayState' => relayState)) end end ``` @@ -838,7 +787,7 @@ def process_logout_response settings = Account.get_saml_settings if session.has_key? :transaction_id - logout_response = RubySaml::Logoutresponse.new(params[:SAMLResponse], settings, :matches_request_id => session[:transaction_id]) + logout_response = RubySaml::Logoutresponse.new(params[:SAMLResponse], settings, matches_request_id: session[:transaction_id]) else logout_response = RubySaml::Logoutresponse.new(params[:SAMLResponse], settings) end @@ -879,7 +828,7 @@ def idp_logout_request ) if !logout_request.is_valid? logger.error "IdP initiated LogoutRequest was not valid!" - return render :inline => logger.error + return render inline: logger.error end logger.info "IdP initiated Logout for #{logout_request.name_id}" @@ -888,7 +837,7 @@ def idp_logout_request # Generate a response to the IdP. logout_request_id = logout_request.id - logout_response = RubySaml::SloLogoutresponse.new.create(settings, logout_request_id, nil, :RelayState => params[:RelayState]) + logout_response = RubySaml::SloLogoutresponse.new.create(settings, logout_request_id, nil, 'RelayState' => params[:RelayState]) redirect_to logout_response end ``` @@ -913,14 +862,17 @@ end ## Clock Drift -Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition", this may be due to clock differences between your system and that of the Identity Provider. +If during validation of the response you get the error "Current time is earlier than NotBefore condition", +this may be due to clock differences between your system and that of the IdP. -First, ensure that both systems synchronize their clocks, using for example the industry standard [Network Time Protocol (NTP)](http://en.wikipedia.org/wiki/Network_Time_Protocol). +First, ensure that both systems synchronize their clocks, using for example the industry standard +[Network Time Protocol (NTP)](https://en.wikipedia.org/wiki/Network_Time_Protocol). -Even then you may experience intermittent issues, as 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 by 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: +To allow for a small amount of clock drift, you can initialize the response with the +`:allowed_clock_drift` option, specified in number of seconds. For example: ```ruby -response = RubySaml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1.second) +response = 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. @@ -936,7 +888,7 @@ Example: ```ruby def consume response = RubySaml::Response.new(params[:SAMLResponse], { settings: saml_settings }) - ... + # ... end private @@ -956,8 +908,8 @@ settings.attributes_index = 5 settings.attribute_consuming_service.configure do service_name "Service" service_index 5 - add_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" + add_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 ``` @@ -995,3 +947,23 @@ end # Output XML with custom metadata MyMetadata.new.generate(settings) ``` + +## Adding Features, Pull Requests + +* 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 by running `bundle exec rake test`. +* Do not change rakefile, version, or history. +* Open a pull request, following [this template](https://gist.github.com/Lordnibbler/11002759). + +## Attribution + +Portions of the code in `RubySaml::XML` namespace is adapted from earlier work +copyrighted by either Oracle and/or Todd W. Saxton. The original code was distributed +under the Common Development and Distribution License (CDDL) 1.0. This code is planned to +be written entirely in future versions. + +## License + +Ruby SAML is made available under the MIT License. Refer to [LICENSE](LICENSE). diff --git a/UPGRADING.md b/UPGRADING.md index c5b1dbfd..ffdc109c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -97,7 +97,7 @@ request.uuid #=> "my_id_a1b3c5d7-9f1e-3d5c-7b1a-9f1e3d5c7b1a" ``` A side-effect of this change is that the `uuid` of the `Authrequest`, `Logoutrequest`, and `Logoutresponse` -classes now is `nil` until the `#create` method is called (previously, it was set in the constructor.) +classes now is `nil` until the `#create` method is called (previously, it was set in the initializer.) After calling `#create` for the first time the `uuid` will not change, even if a `Settings` object with a different `sp_uuid_prefix` is passed-in on subsequent calls. @@ -113,6 +113,13 @@ The SAML SP request/response message compression behavior is now controlled auto "compression" is used to make redirect URLs which contain SAML messages be shorter. For POST messages, compression may be achieved by enabling `Content-Encoding: gzip` on your webserver. +### Deprecation of IdP certificate fingerprint settings + +The `settings.idp_cert_fingerprint` and `settings.idp_cert_fingerprint_algorithm` are deprecated +and will be removed in RubySaml 2.1.0. Please use `settings.idp_cert` or `settings.idp_cert_multi` instead. +The reasons for this deprecation are that (1) fingerprint cannot be used with HTTP-Redirect binding, +and (2) fingerprint is theoretically susceptible to collision attacks. + ### Other settings deprecations The following parameters in `RubySaml::Settings` are deprecated and will be removed in RubySaml 2.1.0: @@ -215,7 +222,7 @@ the [CVE-2017-11428](https://www.cvedetails.com/cve/CVE-2017-11428/) vulnerabili Version `1.6.0` changes the preferred way to construct instances of `Logoutresponse` and `SloLogoutrequest`. Previously the _SAMLResponse_, _RelayState_, and _SigAlg_ parameters -of these message types were provided via the constructor's `options[:get_params]` parameter. +of these message types were provided via the initializer's `options[:get_params]` parameter. Unfortunately this can result in incompatibility with other SAML implementations; signatures are specified to be computed based on the _sender's_ URI-encoding of the message, which can differ from that of Ruby SAML. In particular, Ruby SAML's URI-encoding does not match that diff --git a/lib/ruby_saml/settings.rb b/lib/ruby_saml/settings.rb index d14d16a3..ddc84598 100644 --- a/lib/ruby_saml/settings.rb +++ b/lib/ruby_saml/settings.rb @@ -36,8 +36,8 @@ def initialize(overrides = {}, keep_security_attributes = false) attr_accessor :idp_slo_service_url attr_accessor :idp_slo_response_service_url attr_accessor :idp_cert - attr_accessor :idp_cert_fingerprint - attr_accessor :idp_cert_fingerprint_algorithm + attr_reader :idp_cert_fingerprint + attr_reader :idp_cert_fingerprint_algorithm attr_accessor :idp_cert_multi attr_accessor :idp_attribute_names attr_accessor :idp_name_qualifier @@ -305,6 +305,18 @@ def get_sp_digest_method end end + # @deprecated Will be removed in v2.1.0 + def idp_cert_fingerprint=(value) + idp_cert_fingerprint_deprecation + @idp_cert_fingerprint = value + end + + # @deprecated Will be removed in v2.1.0 + def idp_cert_fingerprint_algorithm=(value) + idp_cert_fingerprint_deprecation + @idp_cert_fingerprint_algorithm = value + end + # @deprecated Will be removed in v2.1.0 def certificate_new certificate_new_deprecation @@ -349,6 +361,13 @@ def replaced_deprecation(old_param, new_param) "Please set the same value to `RubySaml::Settings##{new_param}` instead." end + # @deprecated Will be removed in v2.1.0 + def idp_cert_fingerprint_deprecation + Logging.deprecate '`RubySaml::Settings#idp_cert_fingerprint` and `#idp_cert_fingerprint_algorithm` are ' \ + 'deprecated and will be removed in RubySaml v2.1.0. Please provide the full IdP certificate in ' \ + '`RubySaml::Settings#idp_cert` instead.' + end + # @deprecated Will be removed in v2.1.0 def certificate_new_deprecation Logging.deprecate '`RubySaml::Settings#certificate_new` is deprecated and will be removed in RubySaml v2.1.0. ' \