ruby-saml-1.15.0/0000755000004100000410000000000014405647360013552 5ustar www-datawww-dataruby-saml-1.15.0/README.md0000644000004100000410000011134714405647360015040 0ustar www-datawww-data# Ruby SAML [![Build Status](https://github.com/onelogin/ruby-saml/actions/workflows/test.yml/badge.svg?query=branch%3Amaster)](https://github.com/onelogin/ruby-saml/actions/workflows/test.yml?query=branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/onelogin/ruby-saml/badge.svg?branch=master)](https://coveralls.io/r/onelogin/ruby-saml?branch=master) Ruby SAML minor and tiny versions may introduce breaking changes. Please read [UPGRADING.md](UPGRADING.md) for guidance on upgrading to new Ruby SAML versions. ## 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. SAML authorization is a two step process and you are expected to implement support for both. We created a demo project for Rails 4 that uses the latest version of this library: [ruby-saml-example](https://github.com/saml-toolkit/ruby-saml-example) ### Supported Ruby Versions The following Ruby versions are covered by CI testing: * 2.1.x * 2.2.x * 2.3.x * 2.4.x * 2.5.x * 2.6.x * 2.7.x * 3.0.x * JRuby 9.1.x * JRuby 9.2.x * TruffleRuby (latest) In addition, the following may work but are untested: * 1.8.7 * 1.9.x * 2.0.x * JRuby 1.7.x * JRuby 9.0.x ## 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 OneLogin::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 ```sh gem install ruby-saml ``` You may require the entire Ruby SAML gem: ```ruby require 'onelogin/ruby-saml' ``` or just the required components individually: ```ruby require 'onelogin/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` ```ruby gem 'nokogiri', '~> 1.5.10' ``` 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 OneLogin::RubySaml::Logging.logger = Logger.new('/var/log/ruby-saml.log') ``` ## The Initialization Phase 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 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): ```ruby def init request = OneLogin::RubySaml::Authrequest.new redirect_to(request.create(saml_settings)) end ``` If the SP knows who should be authenticated in the IdP, then can provide that info as follows: ```ruby def init request = OneLogin::RubySaml::Authrequest.new saml_settings.name_identifier_value_requested = "testuser@example.com" saml_settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" redirect_to(request.create(saml_settings)) 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` methods are specific to your application): ```ruby def consume response = OneLogin::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 else authorize_failure # This method shows an error message # List of errors is available in response.errors array end end ``` In the above there are a few assumptions, one being that `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: ``` response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) response.settings = saml_settings ``` If the assertion of the SAMLResponse is not encrypted, you can initialize the Response without the `:settings` parameter and set it later. If the SAMLResponse contains an encrypted assertion, you need to provide the settings in the initialize method in order to obtain the decrypted assertion, using the service provider private key in order to decrypt. If you don't know what expect, always use the former (set the settings on initialize). ```ruby def saml_settings settings = OneLogin::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.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_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" # or :post, :redirect settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint settings.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 IdPs settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" # or as an array settings.authn_context = [ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", "urn:oasis:names:tc:SAML:2.0:ac:classes:Password" ] # Optional bindings (defaults to Redirect for logout POST for ACS) settings.single_logout_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" # or :post, :redirect settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" # or :post, :redirect settings end ``` The use of settings.issuer is deprecated in favour of settings.sp_entity_id since version 1.11.0 Some assertion validations can be skipped by passing parameters to `OneLogin::RubySaml::Response.new()`. For example, you can skip the `AuthnStatement`, `Conditions`, `Recipient`, or the `SubjectConfirmation` validations by initializing the response with different options: ```ruby response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_authnstatement: true}) # skips AuthnStatement response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation response = OneLogin::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 = OneLogin::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: ```ruby # This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application. class SamlController < ApplicationController def init request = OneLogin::RubySaml::Authrequest.new redirect_to(request.create(saml_settings)) end def consume response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) response.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 else authorize_failure # This method shows an error message # List of errors is available in response.errors array end end private def saml_settings settings = OneLogin::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.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" # 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.configure do service_name "Service" service_index 5 add_attribute :name => "Name", :name_format => "Name Format", :friendly_name => "Friendly Name" end settings end 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. 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 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. ## Handling Multiple IdP Certificates If the IdP metadata XML includes multiple certificates, you may specify the `idp_cert_multi` parameter. When used, the `idp_cert` and `idp_cert_fingerprint` parameters are ignored. This is useful in the following scenarios: * The IdP uses different certificates for signing versus encryption. * The IdP is undergoing a key rollover and is publishing the old and new certificates in parallel. The `idp_cert_multi` must be a `Hash` as follows. The `:signing` and `:encryption` arrays below, add the IdP X.509 public certificates which were published in the IdP metadata. ```ruby { :signing => [], :encryption => [] } ``` ## Metadata Based Configuration The method above requires a little extra work to manually specify attributes about both the IdP and your SP application. There's an easier method: use a metadata exchange. Metadata is 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 `IdpMetadataParser#parse_remote`, the IdP metadata will be added to the settings. ```ruby def saml_settings idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new # Returns OneLogin::RubySaml::Settings pre-populated with IdP metadata settings = idp_metadata_parser.parse_remote("https://example.com/auth/saml2/idp/metadata") settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume" settings.sp_entity_id = "http://#{request.host}/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" settings end ``` The following attributes are set: * idp_entity_id * name_identifier_format * idp_sso_service_url * idp_slo_service_url * idp_attribute_names * idp_cert * idp_cert_fingerprint * idp_cert_multi ### Retrieve one Entity Descriptor when many exist in Metadata If the Metadata contains several entities, the relevant Entity Descriptor can be specified when retrieving the settings from the IdpMetadataParser by its Entity Id value: ```ruby validate_cert = true settings = idp_metadata_parser.parse_remote( "https://example.com/auth/saml2/idp/metadata", validate_cert, entity_id: "http//example.com/target/entity" ) ``` ### Parsing Metadata into an Hash The `OneLogin::RubySaml::IdpMetadataParser` also provides the methods `#parse_to_hash` and `#parse_remote_to_hash`. 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, but it can be done as follows: * Download the XML. * Validate the Signature, providing the cert. * Provide the XML to the parse method if the signature was validated ``` require "xml_security" require "onelogin/ruby-saml/utils" require "onelogin/ruby-saml/idp_metadata_parser" url = "" idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new uri = URI.parse(url) raise ArgumentError.new("url must begin with http or https") unless /^https?/ =~ uri.scheme http = Net::HTTP.new(uri.host, uri.port) if uri.scheme == "https" http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_PEER end get = Net::HTTP::Get.new(uri.request_uri) get.basic_auth uri.user, uri.password if uri.user response = http.request(get) xml = response.body errors = [] doc = XMLSecurity::SignedDocument.new(xml, errors) cert_str = "" cert = OneLogin::RubySaml::Utils.format_cert("cert_str") metadata_sign_cert = OpenSSL::X509::Certificate.new(cert) valid = doc.validate_document_with_cert(metadata_sign_cert, true) if valid settings = idp_metadata_parser.parse( xml, entity_id: "" ) else print "Metadata Signarture failed to be verified with the cert provided" end ## Retrieving Attributes If you are using `saml:AttributeStatement` to transfer data like the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the `single_value_compatibility` (when activated, only the first value is returned) ```ruby response = OneLogin::RubySaml::Response.new(params[:SAMLResponse]) response.settings = saml_settings response.attributes[:username] ``` Imagine this `saml:AttributeStatement` ```xml demo value1 value2 role1 role2 role3 valuePresent usersName ``` ```ruby 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] "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"=>["usersName"]}> # Active single_value_compatibility OneLogin::RubySaml::Attributes.single_value_compatibility = true pp(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.fetch(:role)) # => "role1" pp(response.attributes[:attribute_with_nil_value]) # => nil pp(response.attributes[:attribute_with_nils_and_empty_strings]) # => "" pp(response.attributes[:not_exists]) # => nil pp(response.attributes.single(:not_exists)) # => nil pp(response.attributes.multi(:not_exists)) # => nil pp(response.attributes.fetch(/givenname/)) # => "usersName" # Deprecated single_value_compatibility OneLogin::RubySaml::Attributes.single_value_compatibility = false pp(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.fetch(: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]) # => nil pp(response.attributes.single(:not_exists)) # => nil pp(response.attributes.multi(:not_exists)) # => nil pp(response.attributes.fetch(/givenname/)) # => ["usersName"] ``` The `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 `settings.authn_context_comparison` parameter. Possible values include: 'exact', 'better', 'maximum' and 'minimum' (default value is 'exact'). 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 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) 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 metadata will be polled by the IdP every few minutes, so updating your settings should propagate to the IdP settings. ```ruby class SamlController < ApplicationController # ... the rest of your controller definitions ... def metadata settings = Account.get_saml_settings meta = OneLogin::RubySaml::Metadata.new render :xml => meta.generate(settings), :content_type => "application/samlmetadata+xml" end 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) ``` ## Signing and Decryption Ruby SAML supports the following functionality: 1. Signing your SP Metadata XML 2. Signing your SP SAML messages 3. Decrypting IdP Assertion messages upon receipt (EncryptedAssertion) 4. Verifying signatures on SAML messages and IdP Assertions 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" ``` Note that the same certificate (and its associated private key) are used to perform all decryption and signing-related functions (1-4) above. Ruby SAML does not currently allow 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] = XMLSecurity::Document::SHA1 settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 ``` #### Signing SP Metadata 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.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. 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" ``` 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 ``` 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. #### 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. 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.security[:want_assertions_encrypted] = true # Invalidate SAML messages without an EncryptedAssertion ``` #### Verifying Signature on IdP Assertions You may require the IdP to sign its SAML Assertions using the following setting. With will add `` to your SP Metadata XML. The signature will be checked against the `` element present in the IdP's metadata. ```ruby settings.security[:want_assertions_signed] = true # Require the IdP to sign its SAML Assertions ``` #### Certificate and Signature Validation 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 ``` By default, Ruby SAML will raise a `OneLogin::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 ``` #### Audience Validation A service provider should only consider a SAML response valid if the IdP includes an element containting an element that uniquely identifies the service provider. Unless you specify the `skip_audience` option, Ruby SAML will validate that each SAML response includes an element whose contents matches `settings.sp_entity_id`. By default, Ruby SAML considers an element containing only empty elements to be valid. That means an otherwise valid SAML response with a condition like this would be valid: ```xml ``` You may enforce that an element containing only empty elements is invalid using the `settings.security[:strict_audience_validation]` parameter. ```ruby settings.security[:strict_audience_validation] = true ``` #### Key Rollover To update the SP X.509 certificate and private key without disruption of service, you may define the parameter `settings.certificate_new`. This will publish the new SP certificate in your metadata so that your IdP counterparties may cache it in preparation for rollover. For example, if you to rollover from `CERT A` to `CERT B`. Before rollover, your settings should look as follows. Both `CERT A` and `CERT B` will now appear in your SP metadata, however `CERT A` will still be used for signing and encryption at this time. ```ruby settings.certificate = "CERT A" settings.private_key = "PRIVATE KEY FOR CERT A" settings.certificate_new = "CERT B" ``` After the IdP has cached `CERT B`, you may then change your settings as follows: ```ruby settings.certificate = "CERT B" settings.private_key = "PRIVATE KEY FOR CERT B" ``` ## Single Log Out Ruby SAML 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: ```ruby # Create a SP initiated SLO def sp_logout_request # LogoutRequest accepts plain browser requests w/o paramters settings = saml_settings if settings.idp_slo_service_url.nil? logger.info "SLO IdP Endpoint not found in settings, executing then a normal logout'" delete_session else logout_request = OneLogin::RubySaml::Logoutrequest.new logger.info "New SP SLO for userid '#{session[:userid]}' transactionid '#{logout_request.uuid}'" if settings.name_identifier_value.nil? settings.name_identifier_value = session[:userid] end # Ensure user is logged out before redirect to IdP, in case anything goes wrong during single logout process (as recommended by saml2int [SDP-SP34]) logged_user = session[:userid] logger.info "Delete session for '#{session[:userid]}'" delete_session # Save the transaction_id to compare it with the response we get back session[:transaction_id] = logout_request.uuid session[:logged_out_user] = logged_user relayState = url_for(controller: 'saml', action: 'index') redirect_to(logout_request.create(settings, :RelayState => relayState)) end end ``` This method processes the SAML Logout Response sent by the IdP as the reply of the SAML Logout Request: ```ruby # After sending an SP initiated LogoutRequest to the IdP, we need to accept # the LogoutResponse, verify it, then actually delete our session. def process_logout_response settings = Account.get_saml_settings if session.has_key? :transaction_id logout_response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings, :matches_request_id => session[:transaction_id]) else logout_response = OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings) end logger.info "LogoutResponse is: #{logout_response.to_s}" # Validate the SAML Logout Response if not logout_response.validate logger.error "The SAML Logout Response is invalid" else # Actually log out this session logger.info "SLO completed for '#{session[:logged_out_user]}'" delete_session end end # Delete a user's session. def delete_session session[:userid] = nil session[:attributes] = nil session[:transaction_id] = nil session[:logged_out_user] = nil end ``` Here is an example that we could add to our previous controller to process a SAML Logout Request from the IdP and reply with a SAML Logout Response to the IdP: ```ruby # Method to handle IdP initiated logouts def idp_logout_request settings = Account.get_saml_settings # ADFS URL-Encodes SAML data as lowercase, and the toolkit by default uses # uppercase. Turn it True for ADFS compatibility on signature verification settings.security[:lowercase_url_encoding] = true logout_request = OneLogin::RubySaml::SloLogoutrequest.new( params[:SAMLRequest], settings: settings ) if !logout_request.is_valid? logger.error "IdP initiated LogoutRequest was not valid!" return render :inline => logger.error end logger.info "IdP initiated Logout for #{logout_request.name_id}" # Actually log out this session delete_session # Generate a response to the IdP. logout_request_id = logout_request.id logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request_id, nil, :RelayState => params[:RelayState]) redirect_to logout_response end ``` All the mentioned methods could be handled in a unique view: ```ruby # Trigger SP and IdP initiated Logout requests def logout # If we're given a logout request, handle it in the IdP logout initiated method if params[:SAMLRequest] return idp_logout_request # We've been given a response back from the IdP, process it elsif params[:SAMLResponse] return process_logout_response # Initiate SLO (send Logout Request) else return sp_logout_request end 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. 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). 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: ```ruby 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. ## Deflation Limit To protect against decompression bombs (a form of DoS attack), SAML messages are limited to 250,000 bytes by default. Sometimes legitimate SAML messages will exceed this limit, for example due to custom claims like including groups a user is a member of. If you want to customize this limit, you need to provide a different setting when initializing the response object. Example: ```ruby def consume response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], { settings: saml_settings }) ... end private def saml_settings OneLogin::RubySaml::Settings.new(message_max_bytesize: 500_000) end ``` ## Attribute Service 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. ```ruby settings = OneLogin::RubySaml::Settings.new 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" end ``` The `attribute_value` option additionally accepts an array of possible values. ## Custom Metadata Fields Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.) into the SP metadata. This can be achieved by extending the `OneLogin::RubySaml::Metadata` class and overriding the `#add_extras` method as per the following example: ```ruby class MyMetadata < OneLogin::RubySaml::Metadata def add_extras(root, _settings) org = root.add_element("md:Organization") org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.' org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME' org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com' cp = root.add_element("md:ContactPerson", 'contactType' => 'technical') cp.add_element("md:GivenName").text = 'ACME SAML Team' cp.add_element("md:EmailAddress").text = 'saml@acme.com' end end # Output XML with custom metadata MyMetadata.new.generate(settings) ``` ruby-saml-1.15.0/gemfiles/0000755000004100000410000000000014405647360015345 5ustar www-datawww-dataruby-saml-1.15.0/gemfiles/nokogiri-1.5.gemfile0000644000004100000410000000012314405647360021015 0ustar www-datawww-datasource 'https://rubygems.org' gem "nokogiri", "~> 1.5.10" gemspec :path => "../" ruby-saml-1.15.0/CHANGELOG.md0000644000004100000410000004765214405647360015401 0ustar www-datawww-data# Ruby SAML Changelog ### 1.15.0 (Jan 04, 2023) * [#650](https://github.com/SAML-Toolkits/ruby-saml/pull/650) Replace strip! by strip on compute_digest method * [#638](https://github.com/SAML-Toolkits/ruby-saml/pull/638) Fix dateTime format for the validUntil attribute of the generated metadata * [#576](https://github.com/SAML-Toolkits/ruby-saml/pull/576) Support idp cert multi with string keys * [#567](https://github.com/SAML-Toolkits/ruby-saml/pull/567) Improve Code quality * Add info about new repo, new maintainer, new security contact * Fix tests, Adjust dependencies, Add ruby 3.2 and new jruby versions tests to the CI. Add coveralls support ### 1.14.0 (Feb 01, 2022) * [#627](https://github.com/onelogin/ruby-saml/pull/627) Support escape downcasing for validating SLO Signatures of ADFS/Azure * [#633](https://github.com/onelogin/ruby-saml/pull/633) Support ability to change ID prefix * Make the uuid editable on the SAML Messages generated by the toolkit * [#622](https://github.com/onelogin/ruby-saml/pull/622) Add security setting to more strictly enforce audience validation ### 1.13.0 (Sept 06, 2021) * [#611](https://github.com/onelogin/ruby-saml/pull/601) Replace MAX_BYTE_SIZE constant with setting: message_max_bytesize * [#605](https://github.com/onelogin/ruby-saml/pull/605) :allowed_clock_drift is now bidrectional * [#614](https://github.com/onelogin/ruby-saml/pull/614) Support :name_id_format option for IdpMetadataParser * [#611](https://github.com/onelogin/ruby-saml/pull/611) IdpMetadataParser should always set idp_cert_multi, even when there is only one cert * [#610](https://github.com/onelogin/ruby-saml/pull/610) New IDP sso/slo binding params which deprecate :embed_sign * [#602](https://github.com/onelogin/ruby-saml/pull/602) Refactor the OneLogin::RubySaml::Metadata class * [#586](https://github.com/onelogin/ruby-saml/pull/586) Support milliseconds in cacheDuration parsing * [#585](https://github.com/onelogin/ruby-saml/pull/585) Do not append " | " to StatusCode unnecessarily * [#607](https://github.com/onelogin/ruby-saml/pull/607) Clean up * Add warning about the use of IdpMetadataParser class and SSRF * CI: Migrate from Travis to Github Actions ### 1.12.2 (Apr 08, 2021) * [#575](https://github.com/onelogin/ruby-saml/pull/575) Fix SloLogoutresponse bug on LogoutRequest ### 1.12.1 (Apr 05, 2021) * Fix XPath typo incompatible with Rexml 3.2.5 * Refactor GCM support ### 1.12.0 (Feb 18, 2021) * Support AES-128-GCM, AES-192-GCM, and AES-256-GCM encryptions * Parse & return SLO ResponseLocation in IDPMetadataParser & Settings * Adding idp_sso_service_url and idp_slo_service_url settings * [#536](https://github.com/onelogin/ruby-saml/pull/536) Adding feth method to be able retrieve attributes based on regex * Reduce size of built gem by excluding the test folder * Improve protection on Zlib deflate decompression bomb attack. * Add ValidUntil and cacheDuration support on Metadata generator * Add support for cacheDuration at the IdpMetadataParser * Support customizable statusCode on generated LogoutResponse * [#545](https://github.com/onelogin/ruby-saml/pull/545) More specific error messages for signature validation * Support Process Transform * Raise SettingError if invoking an action with no endpoint defined on the settings * Made IdpMetadataParser more extensible for subclasses *[#548](https://github.com/onelogin/ruby-saml/pull/548) Add :skip_audience option * [#555](https://github.com/onelogin/ruby-saml/pull/555) Define 'soft' variable to prevent exception when doc cert is invalid * Improve documentation ### 1.11.0 (Jul 24, 2019) * Deprecate settings.issuer in favor of settings.sp_entity_id * Add support for certification expiration ### 1.10.2 (Apr 29, 2019) * Add valid until, accessor * Fix Rubygem metadata that requested nokogiri <= 1.5.11 ### 1.10.1 (Apr 08, 2019) * Fix ruby 1.8.7 incompatibilities ### 1.10.0 (Mar 21, 2019) * Add Subject support on AuthNRequest to allow SPs provide info to the IdP about the user to be authenticated * Improves IdpMetadataParser to allow parse multiple IDPSSODescriptors * Improves format_cert method to accept certs with /\x0d/ * Forces nokogiri >= 1.8.2 when possible ### 1.9.0 (Sept 03, 2018) * [#458](https://github.com/onelogin/ruby-saml/pull/458) Remove ruby 2.4+ warnings * Improve JRuby support * [#465](https://github.com/onelogin/ruby-saml/pull/465) Extend Settings initialization with the new keep_security_attributes parameter * Fix wrong message when SessionNotOnOrAfter expired * [#471](https://github.com/onelogin/ruby-saml/pull/471) Allow for `allowed_clock_drift` to be set as a string ### 1.8.0 (April 23, 2018) * [#437](https://github.com/onelogin/ruby-saml/issues/437) Creating AuthRequests/LogoutRequests/LogoutResponses with nil RelayState should not send empty RelayState URL param * [#454](https://github.com/onelogin/ruby-saml/pull/454) Added Response available options * [#453](https://github.com/onelogin/ruby-saml/pull/453) Raise a more descriptive exception if idp_sso_target_url is missing * [#452](https://github.com/onelogin/ruby-saml/pull/452) Fix behavior of skip_conditions flag on Response * [#449](https://github.com/onelogin/ruby-saml/pull/449) Add ability to skip authnstatement validation * Clear cached values to be able to use IdpMetadataParser more than once * Updated invalid audience error message ### 1.7.2 (Feb 28, 2018) * [#446](https://github.com/onelogin/ruby-saml/pull/446) Normalize text returned by OneLogin::RubySaml::Utils.element_text ### 1.7.1 (Feb 28, 2018) * [#444](https://github.com/onelogin/ruby-saml/pull/444) Fix audience validation for empty audience restriction ### 1.7.0 (Feb 27, 2018) * Fix vulnerability CVE-2017-11428. Process text of nodes properly, ignoring comments ### 1.6.1 (January 15, 2018) * [#428](https://github.com/onelogin/ruby-saml/issues/428) Fix a bug on IdPMetadataParser when parsing certificates * [#426](https://github.com/onelogin/ruby-saml/pull/426) Ensure `Rails` responds to `logger` ### 1.6.0 (November 27, 2017) * [#418](https://github.com/onelogin/ruby-saml/pull/418) Improve SAML message signature validation using original encoded parameters instead decoded in order to avoid conflicts (URL-encoding is not canonical, reported issues with ADFS) * [#420](https://github.com/onelogin/ruby-saml/pull/420) Expose NameID Format on SloLogoutrequest * [#423](https://github.com/onelogin/ruby-saml/pull/423) Allow format_cert to work with chained certificates * [#422](https://github.com/onelogin/ruby-saml/pull/422) Use to_s for requested attribute value ### 1.5.0 (August 31, 2017) * [#400](https://github.com/onelogin/ruby-saml/pull/400) When validating Signature use stored IdP certficate if Signature contains no info about Certificate * [#402](https://github.com/onelogin/ruby-saml/pull/402) Fix validate_response_state method that rejected SAMLResponses when using idp_cert_multi and idp_cert and idp_cert_fingerprint were not provided. * [#411](https://github.com/onelogin/ruby-saml/pull/411) Allow space in Base64 string * [#407](https://github.com/onelogin/ruby-saml/issues/407) Improve IdpMetadataParser raising an ArgumentError when parser method receive a metadata string with no IDPSSODescriptor element. * [#374](https://github.com/onelogin/ruby-saml/issues/374) Support more than one level of StatusCode * [#405](https://github.com/onelogin/ruby-saml/pull/405) Support ADFS encrypted key (Accept KeyInfo nodes with no ds namespace) ### 1.4.3 (May 18, 2017) * Added SubjectConfirmation Recipient validation * [#393](https://github.com/onelogin/ruby-saml/pull/393) Implement IdpMetadataParser#parse_to_hash * Adapt IdP XML metadata parser to take care of multiple IdP certificates and be able to inject the data obtained on the settings. * Improve binding detection on idp metadata parser * [#373](https://github.com/onelogin/ruby-saml/pull/373) Allow metadata to be retrieved from source containing data for multiple entities * Be able to register future SP x509cert on the settings and publish it on SP metadata * Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption. * Improve regex to detect base64 encoded messages * Fix binding configuration example in README.md * Add Fix SLO request. Correct NameQualifier/SPNameQualifier values. * Validate serial number as string to work around libxml2 limitation * Propagate isRequired on md:RequestedAttribute when generating SP metadata ### 1.4.2 (January 11, 2017) * Improve tests format * Fix nokogiri requirements based on ruby version * Only publish `KeyDescriptor[use="encryption"]` at SP metadata if `security[:want_assertions_encrypted]` is true * Be able to skip destination validation * Improved inResponse validation on SAMLResponses and LogoutResponses * [#354](https://github.com/onelogin/ruby-saml/pull/354) Allow scheme and domain to match ignoring case * [#363](https://github.com/onelogin/ruby-saml/pull/363) Add support for multiple requested attributes ### 1.4.1 (October 19, 2016) * [#357](https://github.com/onelogin/ruby-saml/pull/357) Add EncryptedAttribute support. Improve decrypt method * Allow multiple authn_context_decl_ref in settings * Allow options[:settings] to be an hash for Settings overrides in IdpMetadataParser#parse * Recover issuers method ### 1.4.0 (October 13, 2016) * Several security improvements: * Conditions element required and unique. * AuthnStatement element required and unique. * SPNameQualifier must math the SP EntityID * Reject saml:Attribute element with same “Name” attribute * Reject empty nameID * Require Issuer element. (Must match IdP EntityID). * Destination value can't be blank (if present must match ACS URL). * Check that the EncryptedAssertion element only contains 1 Assertion element. * [#335](https://github.com/onelogin/ruby-saml/pull/335) Explicitly parse as XML and fix setting of Nokogiri options. * [#345](https://github.com/onelogin/ruby-saml/pull/345)Support multiple settings.auth_context * More tests to prevent XML Signature Wrapping * [#342](https://github.com/onelogin/ruby-saml/pull/342) Correct the usage of Mutex * [352](https://github.com/onelogin/ruby-saml/pull/352) Support multiple AttributeStatement tags ### 1.3.1 (July 10, 2016) * Fix response_test.rb of gem 1.3.0 * Add reference to Security Guidelines * Update License * [#334](https://github.com/onelogin/ruby-saml/pull/334) Keep API backward-compatibility on IdpMetadataParser fingerprint method. ### 1.3.0 (June 24, 2016) * [Security Fix](https://github.com/onelogin/ruby-saml/commit/a571f52171e6bfd87db59822d1d9e8c38fb3b995) Add extra validations to prevent Signature wrapping attacks * Fix XMLSecurity SHA256 and SHA512 uris * [#326](https://github.com/onelogin/ruby-saml/pull/326) Fix Destination validation ### 1.2.0 (April 29, 2016) * [#269](https://github.com/onelogin/ruby-saml/pull/269) Refactor error handling; allow collect error messages when soft=true (normal validation stop after find first error) * [#289](https://github.com/onelogin/ruby-saml/pull/289) Remove uuid gem in favor of SecureRandom * [#297](https://github.com/onelogin/ruby-saml/pull/297) Implement EncryptedKey RetrievalMethod support * [#298](https://github.com/onelogin/ruby-saml/pull/298) IDP metadata parsing improved: binding parsing, fingerprint_algorithm support) * [#299](https://github.com/onelogin/ruby-saml/pull/299) Make 'signing' at KeyDescriptor optional * [#308](https://github.com/onelogin/ruby-saml/pull/308) Support name_id_format on SAMLResponse * [#315](https://github.com/onelogin/ruby-saml/pull/315) Support for canonicalization with comments * [#316](https://github.com/onelogin/ruby-saml/pull/316) Fix Misspelling of transation_id to transaction_id * [#321](https://github.com/onelogin/ruby-saml/pull/321) Support Attribute Names on IDPSSODescriptor parser * Changes on empty URI of Signature reference management * [#320](https://github.com/onelogin/ruby-saml/pull/320) Dont mutate document to fix lack of reference URI * [#306](https://github.com/onelogin/ruby-saml/pull/306) Support WantAssertionsSigned ### 1.1.2 (February 15, 2016) * Improve signature validation. Add tests. [#302](https://github.com/onelogin/ruby-saml/pull/302) Add Destination validation. * [#292](https://github.com/onelogin/ruby-saml/pull/292) Improve the error message when validating the audience. * [#287](https://github.com/onelogin/ruby-saml/pull/287) Keep the extracted certificate when parsing IdP metadata. ### 1.1.1 (November 10, 2015) * [#275](https://github.com/onelogin/ruby-saml/pull/275) Fix a bug on signature validations that invalidates valid SAML messages. ### 1.1.0 (October 27, 2015) * [#273](https://github.com/onelogin/ruby-saml/pull/273) Support SAMLResponse without ds:x509certificate * [#270](https://github.com/onelogin/ruby-saml/pull/270) Allow SAML elements to come from any namespace (at decryption process) * [#261](https://github.com/onelogin/ruby-saml/pull/261) Allow validate_subject_confirmation Response validation to be skipped * [#258](https://github.com/onelogin/ruby-saml/pull/258) Fix allowed_clock_drift on the validate_session_expiration test * [#256](https://github.com/onelogin/ruby-saml/pull/256) Separate the create_authentication_xml_doc in two methods. * [#255](https://github.com/onelogin/ruby-saml/pull/255) Refactor validate signature. * [#254](https://github.com/onelogin/ruby-saml/pull/254) Handle empty URI references * [#251](https://github.com/onelogin/ruby-saml/pull/251) Support qualified and unqualified NameID in attributes * [#234](https://github.com/onelogin/ruby-saml/pull/234) Add explicit support for JRuby ### 1.0.0 (June 30, 2015) * [#247](https://github.com/onelogin/ruby-saml/pull/247) Avoid entity expansion (XEE attacks) * [#246](https://github.com/onelogin/ruby-saml/pull/246) Fix bug generating Logout Response (issuer was at wrong order) * [#243](https://github.com/onelogin/ruby-saml/issues/243) and [#244](https://github.com/onelogin/ruby-saml/issues/244) Fix metadata builder errors. Fix metadata xsd. * [#241](https://github.com/onelogin/ruby-saml/pull/241) Add decrypt support (EncryptID and EncryptedAssertion). Improve compatibility with namespaces. * [#240](https://github.com/onelogin/ruby-saml/pull/240) and [#238](https://github.com/onelogin/ruby-saml/pull/238) Improve test coverage and refactor. * [#239](https://github.com/onelogin/ruby-saml/pull/239) Improve security: Add more validations to SAMLResponse, LogoutRequest and LogoutResponse. Refactor code and improve tests coverage. * [#237](https://github.com/onelogin/ruby-saml/pull/237) Don't pretty print metadata by default. * [#235](https://github.com/onelogin/ruby-saml/pull/235) Remove the soft parameter from validation methods. Now can be configured on the settings and each class read it and store as an attribute of the class. Adding some validations and refactor old ones. * [#232](https://github.com/onelogin/ruby-saml/pull/232) Improve validations: Store the causes in the errors array, code refactor * [#231](https://github.com/onelogin/ruby-saml/pull/231) Refactor HTTP-Redirect Sign method, Move test data to right folder * [#226](https://github.com/onelogin/ruby-saml/pull/226) Ensure IdP certificate is formatted properly * [#225](https://github.com/onelogin/ruby-saml/pull/225) Add documentation to several methods. Fix xpath injection on xml_security.rb * [#223](https://github.com/onelogin/ruby-saml/pull/223) Allow logging to be delegated to an arbitrary Logger * [#222](https://github.com/onelogin/ruby-saml/pull/222) No more silent failure fetching idp metadata (OneLogin::RubySaml::HttpError raised). ### 0.9.2 (Apr 28, 2015) * [#216](https://github.com/onelogin/ruby-saml/pull/216) Add fingerprint algorithm support * [#218](https://github.com/onelogin/ruby-saml/pull/218) Update README.md * [#214](https://github.com/onelogin/ruby-saml/pull/214) Cleanup `SamlMessage` class * [#213](https://github.com/onelogin/ruby-saml/pull/213) Add ability to sign metadata. (Improved) * [#212](https://github.com/onelogin/ruby-saml/pull/212) Rename library entry point * [#210](https://github.com/onelogin/ruby-saml/pull/210) Call assert in tests * [#208](https://github.com/onelogin/ruby-saml/pull/208) Update tests and CI for Ruby 2.2.0 * [#205](https://github.com/onelogin/ruby-saml/pull/205) Allow requirement of single files * [#204](https://github.com/onelogin/ruby-saml/pull/204) Require ‘net/http’ library * [#201](https://github.com/onelogin/ruby-saml/pull/201) Freeze and duplicate default security settings hash so that it doesn't get modified. * [#200](https://github.com/onelogin/ruby-saml/pull/200) Set default SSL certificate store in Ruby 1.8. * [#199](https://github.com/onelogin/ruby-saml/pull/199) Change Nokogiri's runtime dependency to fix support for Ruby 1.8.7. * [#179](https://github.com/onelogin/ruby-saml/pull/179) Add support for setting the entity ID and name ID format when parsing metadata * [#175](https://github.com/onelogin/ruby-saml/pull/175) Introduce thread safety to SAML schema validation * [#171](https://github.com/onelogin/ruby-saml/pull/171) Fix inconsistent results with using regex matches in decode_raw_saml ### 0.9.1 (Feb 10, 2015) * [#194](https://github.com/onelogin/ruby-saml/pull/194) Relax nokogiri gem requirements * [#191](https://github.com/onelogin/ruby-saml/pull/191) Use Minitest instead of Test::Unit ### 0.9 (Jan 26, 2015) * [#169](https://github.com/onelogin/ruby-saml/pull/169) WantAssertionSigned should be either true or false * [#167](https://github.com/onelogin/ruby-saml/pull/167) (doc update) make unit of clock drift obvious * [#160](https://github.com/onelogin/ruby-saml/pull/160) Extended solution for Attributes method [] can raise NoMethodError * [#158](https://github.com/onelogin/ruby-saml/pull/1) Added ability to specify attribute services in metadata * [#154](https://github.com/onelogin/ruby-saml/pull/154) Fix incorrect gem declaration statement * [#152](https://github.com/onelogin/ruby-saml/pull/152) Fix the PR #99 * [#150](https://github.com/onelogin/ruby-saml/pull/150) Nokogiri already in gemspec * [#147](https://github.com/onelogin/ruby-saml/pull/147) Fix LogoutResponse issuer validation and implement SAML Response issuer validation. * [#144](https://github.com/onelogin/ruby-saml/pull/144) Fix DigestMethod lookup bug * [#139](https://github.com/onelogin/ruby-saml/pull/139) Fixes handling of some soft and hard validation failures * [#138](https://github.com/onelogin/ruby-saml/pull/138) Change logoutrequest.rb to UTC time * [#136](https://github.com/onelogin/ruby-saml/pull/136) Remote idp metadata * [#135](https://github.com/onelogin/ruby-saml/pull/135) Restored support for NIL as well as empty AttributeValues * [#134](https://github.com/onelogin/ruby-saml/pull/134) explicitly require "onelogin/ruby-saml/logging" * [#133](https://github.com/onelogin/ruby-saml/pull/133) Added license to gemspec * [#132](https://github.com/onelogin/ruby-saml/pull/132) Support AttributeConsumingServiceIndex in AuthnRequest * [#131](https://github.com/onelogin/ruby-saml/pull/131) Add ruby 2.1.1 to .travis.yml * [#122](https://github.com/onelogin/ruby-saml/pull/122) Fixes #112 and #117 in a backwards compatible manner * [#119](https://github.com/onelogin/ruby-saml/pull/119) Add support for extracting IdP details from metadata xml ### 0.8.2 (Jan 26, 2015) * [#183](https://github.com/onelogin/ruby-saml/pull/183) Resolved a security vulnerability where string interpolation in a `REXML::XPath.first()` method call allowed for arbitrary code execution. ### 0.8.0 (Feb 21, 2014) **IMPORTANT**: This release changed namespace of the gem from `OneLogin::Saml` to `OneLogin::RubySaml`. Please update your implementations of the gem accordingly. * [#111](https://github.com/onelogin/ruby-saml/pull/111) `Onelogin::` is `OneLogin::` * [#108](https://github.com/onelogin/ruby-saml/pull/108) Change namespacing from `Onelogin::Saml` to `Onelogin::Rubysaml` ### 0.7.3 (Feb 20, 2014) Updated gem dependencies to be compatible with Ruby 1.8.7-p374 and 1.9.3-p448. Removed unnecessary `canonix` gem dependency. * [#107](https://github.com/onelogin/ruby-saml/pull/107) Relax nokogiri version requirement to >= 1.5.0 * [#105](https://github.com/onelogin/ruby-saml/pull/105) Lock Gem versions, fix to resolve possible namespace collision ruby-saml-1.15.0/.gitignore0000644000004100000410000000017614405647360015546 0ustar www-datawww-data*.sw? .DS_Store coverage rdoc pkg Gemfile.lock gemfiles/*.lock .idea/* lib/Lib.iml test/Test.iml .rvmrc *.gem .bundle *.patch ruby-saml-1.15.0/LICENSE0000644000004100000410000000212514405647360014557 0ustar www-datawww-dataCopyright (c) 2010-2022 OneLogin, Inc. Copyright (c) 2023 IAM Digital Services, SL. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-saml-1.15.0/.document0000644000004100000410000000007414405647360015372 0ustar www-datawww-dataREADME.rdoc lib/**/*.rb bin/* features/**/*.feature LICENSE ruby-saml-1.15.0/Rakefile0000644000004100000410000000104114405647360015213 0ustar www-datawww-datarequire 'rubygems' require 'rake' #not being used yet. require 'rake/testtask' Rake::TestTask.new(:test) do |test| test.libs << 'lib' << 'test' test.pattern = 'test/**/*_test.rb' test.verbose = true end begin require 'rcov/rcovtask' Rcov::RcovTask.new do |test| test.libs << 'test' test.pattern = 'test/**/*_test.rb' test.verbose = true end rescue LoadError task :rcov do abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" end end task :test task :default => :test ruby-saml-1.15.0/lib/0000755000004100000410000000000014405647360014320 5ustar www-datawww-dataruby-saml-1.15.0/lib/xml_security.rb0000644000004100000410000003364714405647360017411 0ustar www-datawww-data# The contents of this file are subject to the terms # of the Common Development and Distribution License # (the License). You may not use this file except in # compliance with the License. # # You can obtain a copy of the License at # https://opensso.dev.java.net/public/CDDLv1.0.html or # opensso/legal/CDDLv1.0.txt # See the License for the specific language governing # permission and limitations under the License. # # When distributing Covered Code, include this CDDL # Header Notice in each file and include the License file # at opensso/legal/CDDLv1.0.txt. # If applicable, add the following below the CDDL Header, # with the fields enclosed by brackets [] replaced by # your own identifying information: # "Portions Copyrighted [year] [name of copyright owner]" # # $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $ # # Copyright 2007 Sun Microsystems Inc. All Rights Reserved # Portions Copyrighted 2007 Todd W Saxton. require 'rubygems' require "rexml/document" require "rexml/xpath" require "openssl" require 'nokogiri' require "digest/sha1" require "digest/sha2" require "onelogin/ruby-saml/utils" require "onelogin/ruby-saml/error_handling" module XMLSecurity class BaseDocument < REXML::Document REXML::Document::entity_expansion_limit = 0 C14N = "http://www.w3.org/2001/10/xml-exc-c14n#" DSIG = "http://www.w3.org/2000/09/xmldsig#" NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONET def canon_algorithm(element) algorithm = element if algorithm.is_a?(REXML::Element) algorithm = element.attribute('Algorithm').value end case algorithm when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" Nokogiri::XML::XML_C14N_1_0 when "http://www.w3.org/2006/12/xml-c14n11", "http://www.w3.org/2006/12/xml-c14n11#WithComments" Nokogiri::XML::XML_C14N_1_1 else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 end end def algorithm(element) algorithm = element if algorithm.is_a?(REXML::Element) algorithm = element.attribute("Algorithm").value end algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i case algorithm when 256 then OpenSSL::Digest::SHA256 when 384 then OpenSSL::Digest::SHA384 when 512 then OpenSSL::Digest::SHA512 else OpenSSL::Digest::SHA1 end end end class Document < BaseDocument RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256' SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384" SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512' ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" INC_PREFIX_LIST = "#default samlp saml ds xs xsi md" attr_writer :uuid def uuid @uuid ||= begin document.root.nil? ? nil : document.root.attributes['ID'] end end # # # # # # # # # # etc. # # # # # def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1) noko = Nokogiri::XML(self.to_s) do |config| config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS end signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG) signed_info_element = signature_element.add_element("ds:SignedInfo") signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N}) signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>signature_method}) # Add Reference reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"}) # Add Transforms transforms_element = reference_element.add_element("ds:Transforms") transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG}) c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N}) c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST}) digest_method_element = reference_element.add_element("ds:DigestMethod", {"Algorithm" => digest_method}) inclusive_namespaces = INC_PREFIX_LIST.split(" ") canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces) reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element)) # add SignatureValue noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config| config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS end noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG) canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N)) signature = compute_signature(private_key, algorithm(signature_method).new, canon_string) signature_element.add_element("ds:SignatureValue").text = signature # add KeyInfo key_info_element = signature_element.add_element("ds:KeyInfo") x509_element = key_info_element.add_element("ds:X509Data") x509_cert_element = x509_element.add_element("ds:X509Certificate") if certificate.is_a?(String) certificate = OpenSSL::X509::Certificate.new(certificate) end x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "") # add the signature issuer_element = elements["//saml:Issuer"] if issuer_element root.insert_after(issuer_element, signature_element) elsif first_child = root.children[0] root.insert_before(first_child, signature_element) else root.add_element(signature_element) end end protected def compute_signature(private_key, signature_algorithm, document) Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "") end def compute_digest(document, digest_algorithm) digest = digest_algorithm.digest(document) Base64.encode64(digest).strip end end class SignedDocument < BaseDocument include OneLogin::RubySaml::ErrorHandling attr_writer :signed_element_id def initialize(response, errors = []) super(response) @errors = errors end def signed_element_id @signed_element_id ||= extract_signed_element_id end def validate_document(idp_cert_fingerprint, soft = true, options = {}) # get cert from response cert_element = REXML::XPath.first( self, "//ds:X509Certificate", { "ds"=>DSIG } ) if cert_element base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element) cert_text = Base64.decode64(base64_cert) begin cert = OpenSSL::X509::Certificate.new(cert_text) rescue OpenSSL::X509::CertificateError => _e return append_error("Document Certificate Error", soft) end if options[:fingerprint_alg] fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(options[:fingerprint_alg]).new else fingerprint_alg = OpenSSL::Digest.new('SHA1') end fingerprint = fingerprint_alg.hexdigest(cert.to_der) # check cert matches registered idp cert if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase return append_error("Fingerprint mismatch", soft) end else if options[:cert] base64_cert = Base64.encode64(options[:cert].to_pem) else if soft return false else return append_error("Certificate element missing in response (ds:X509Certificate) and not cert provided at settings", soft) end end end validate_signature(base64_cert, soft) end def validate_document_with_cert(idp_cert, soft = true) # get cert from response cert_element = REXML::XPath.first( self, "//ds:X509Certificate", { "ds"=>DSIG } ) if cert_element base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element) cert_text = Base64.decode64(base64_cert) begin cert = OpenSSL::X509::Certificate.new(cert_text) rescue OpenSSL::X509::CertificateError => _e return append_error("Document Certificate Error", soft) end # check saml response cert matches provided idp cert if idp_cert.to_pem != cert.to_pem return append_error("Certificate of the Signature element does not match provided certificate", soft) end else base64_cert = Base64.encode64(idp_cert.to_pem) end validate_signature(base64_cert, true) end def validate_signature(base64_cert, soft = true) document = Nokogiri::XML(self.to_s) do |config| config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS end # create a rexml document @working_copy ||= REXML::Document.new(self.to_s).root # get signature node sig_element = REXML::XPath.first( @working_copy, "//ds:Signature", {"ds"=>DSIG} ) # signature method sig_alg_value = REXML::XPath.first( sig_element, "./ds:SignedInfo/ds:SignatureMethod", {"ds"=>DSIG} ) signature_algorithm = algorithm(sig_alg_value) # get signature base64_signature = REXML::XPath.first( sig_element, "./ds:SignatureValue", {"ds" => DSIG} ) signature = Base64.decode64(OneLogin::RubySaml::Utils.element_text(base64_signature)) # canonicalization method canon_algorithm = canon_algorithm REXML::XPath.first( sig_element, './ds:SignedInfo/ds:CanonicalizationMethod', 'ds' => DSIG ) noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG) noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG) canon_string = noko_signed_info_element.canonicalize(canon_algorithm) noko_sig_element.remove # get inclusive namespaces inclusive_namespaces = extract_inclusive_namespaces # check digests ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG}) hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id }) canon_algorithm = canon_algorithm REXML::XPath.first( ref, '//ds:CanonicalizationMethod', { "ds" => DSIG } ) canon_algorithm = process_transforms(ref, canon_algorithm) canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces) digest_algorithm = algorithm(REXML::XPath.first( ref, "//ds:DigestMethod", { "ds" => DSIG } )) hash = digest_algorithm.digest(canon_hashed_element) encoded_digest_value = REXML::XPath.first( ref, "//ds:DigestValue", { "ds" => DSIG } ) digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value)) unless digests_match?(hash, digest_value) return append_error("Digest mismatch", soft) end # get certificate object cert_text = Base64.decode64(base64_cert) cert = OpenSSL::X509::Certificate.new(cert_text) # verify signature unless cert.public_key.verify(signature_algorithm.new, signature, canon_string) return append_error("Key validation error", soft) end return true end private def process_transforms(ref, canon_algorithm) transforms = REXML::XPath.match( ref, "//ds:Transforms/ds:Transform", { "ds" => DSIG } ) transforms.each do |transform_element| if transform_element.attributes && transform_element.attributes["Algorithm"] algorithm = transform_element.attributes["Algorithm"] case algorithm when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" canon_algorithm = Nokogiri::XML::XML_C14N_1_0 when "http://www.w3.org/2006/12/xml-c14n11", "http://www.w3.org/2006/12/xml-c14n11#WithComments" canon_algorithm = Nokogiri::XML::XML_C14N_1_1 when "http://www.w3.org/2001/10/xml-exc-c14n#", "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 end end end canon_algorithm end def digests_match?(hash, digest_value) hash == digest_value end def extract_signed_element_id reference_element = REXML::XPath.first( self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG} ) return nil if reference_element.nil? sei = reference_element.attribute("URI").value[1..-1] sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei end def extract_inclusive_namespaces element = REXML::XPath.first( self, "//ec:InclusiveNamespaces", { "ec" => C14N } ) if element prefix_list = element.attributes.get_attribute("PrefixList").value prefix_list.split(" ") else nil end end end end ruby-saml-1.15.0/lib/schemas/0000755000004100000410000000000014405647360015743 5ustar www-datawww-dataruby-saml-1.15.0/lib/schemas/saml-schema-authn-context-2.0.xsd0000644000004100000410000000137214405647360023754 0ustar www-datawww-data Document identifier: saml-schema-authn-context-2.0 Location: http://docs.oasis-open.org/security/saml/v2.0/ Revision history: V2.0 (March, 2005): New core authentication context schema for SAML V2.0. This is just an include of all types from the schema referred to in the include statement below. ruby-saml-1.15.0/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd0000644000004100000410000000603214405647360023245 0ustar www-datawww-data Document title: Metadata Extension Schema for SAML V2.0 Metadata Extensions for Login and Discovery User Interface Version 1.0 Document identifier: sstc-saml-metadata-ui-v1.0.xsd Location: http://docs.oasis-open.org/security/saml/Post2.0/ Revision history: 16 November 2010: Added Keywords element/type. 01 November 2010 Changed filename. September 2010: Initial version. ruby-saml-1.15.0/lib/schemas/saml-schema-protocol-2.0.xsd0000644000004100000410000003223214405647360023013 0ustar www-datawww-data Document identifier: saml-schema-protocol-2.0 Location: http://docs.oasis-open.org/security/saml/v2.0/ Revision history: V1.0 (November, 2002): Initial Standard Schema. V1.1 (September, 2003): Updates within the same V1.0 namespace. V2.0 (March, 2005): New protocol schema based in a SAML V2.0 namespace. ruby-saml-1.15.0/lib/schemas/sstc-saml-attribute-ext.xsd0000644000004100000410000000133414405647360023171 0ustar www-datawww-data Document title: SAML V2.0 Attribute Extension Schema Document identifier: sstc-saml-attribute-ext.xsd Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security Revision history: V1.0 (October 2008): Initial version. ruby-saml-1.15.0/lib/schemas/xmldsig-core-schema.xsd0000644000004100000410000002342214405647360022321 0ustar www-datawww-data ruby-saml-1.15.0/lib/schemas/saml-schema-metadata-2.0.xsd0000644000004100000410000003715114405647360022737 0ustar www-datawww-data Document identifier: saml-schema-metadata-2.0 Location: http://docs.oasis-open.org/security/saml/v2.0/ Revision history: V2.0 (March, 2005): Schema for SAML metadata, first published in SAML 2.0. ruby-saml-1.15.0/lib/schemas/sstc-metadata-attr.xsd0000644000004100000410000000216314405647360022167 0ustar www-datawww-data Document title: SAML V2.0 Metadata Extention for Entity Attributes Schema Document identifier: sstc-metadata-attr.xsd Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security Revision history: V1.0 (November 2008): Initial version. ruby-saml-1.15.0/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd0000644000004100000410000000265514405647360025037 0ustar www-datawww-data Document title: Metadata Extension Schema for SAML V2.0 Metadata Profile for Algorithm Support Version 1.0 Document identifier: sstc-saml-metadata-algsupport.xsd Location: http://docs.oasis-open.org/security/saml/Post2.0/ Revision history: V1.0 (June 2010): Initial version. ruby-saml-1.15.0/lib/schemas/saml-schema-authn-context-types-2.0.xsd0000644000004100000410000007113314405647360025120 0ustar www-datawww-data Document identifier: saml-schema-authn-context-types-2.0 Location: http://docs.oasis-open.org/security/saml/v2.0/ Revision history: V2.0 (March, 2005): New core authentication context schema types for SAML V2.0. A particular assertion on an identity provider's part with respect to the authentication context associated with an authentication assertion. Refers to those characteristics that describe the processes and mechanisms the Authentication Authority uses to initially create an association between a Principal and the identity (or name) by which the Principal will be known This element indicates that identification has been performed in a physical face-to-face meeting with the principal and not in an online manner. Refers to those characterstics that describe how the 'secret' (the knowledge or possession of which allows the Principal to authenticate to the Authentication Authority) is kept secure This element indicates the types and strengths of facilities of a UA used to protect a shared secret key from unauthorized access and/or use. This element indicates the types and strengths of facilities of a UA used to protect a private key from unauthorized access and/or use. The actions that must be performed before the private key can be used. Whether or not the private key is shared with the certificate authority. In which medium is the key stored. memory - the key is stored in memory. smartcard - the key is stored in a smartcard. token - the key is stored in a hardware token. MobileDevice - the key is stored in a mobile device. MobileAuthCard - the key is stored in a mobile authentication card. This element indicates that a password (or passphrase) has been used to authenticate the Principal to a remote system. This element indicates that a Pin (Personal Identification Number) has been used to authenticate the Principal to some local system in order to activate a key. This element indicates that a hardware or software token is used as a method of identifying the Principal. This element indicates that a time synchronization token is used to identify the Principal. hardware - the time synchonization token has been implemented in hardware. software - the time synchronization token has been implemented in software. SeedLength - the length, in bits, of the random seed used in the time synchronization token. This element indicates that a smartcard is used to identity the Principal. This element indicates the minimum and/or maximum ASCII length of the password which is enforced (by the UA or the IdP). In other words, this is the minimum and/or maximum number of ASCII characters required to represent a valid password. min - the minimum number of ASCII characters required in a valid password, as enforced by the UA or the IdP. max - the maximum number of ASCII characters required in a valid password, as enforced by the UA or the IdP. This element indicates the length of time for which an PIN-based authentication is valid. Indicates whether the password was chosen by the Principal or auto-supplied by the Authentication Authority. principalchosen - the Principal is allowed to choose the value of the password. This is true even if the initial password is chosen at random by the UA or the IdP and the Principal is then free to change the password. automatic - the password is chosen by the UA or the IdP to be cryptographically strong in some sense, or to satisfy certain password rules, and that the Principal is not free to change it or to choose a new password. Refers to those characteristics that define the mechanisms by which the Principal authenticates to the Authentication Authority. The method that a Principal employs to perform authentication to local system components. The method applied to validate a principal's authentication across a network Supports Authenticators with nested combinations of additional complexity. Indicates that the Principal has been strongly authenticated in a previous session during which the IdP has set a cookie in the UA. During the present session the Principal has only been authenticated by the UA returning the cookie to the IdP. Rather like PreviousSession but using stronger security. A secret that was established in a previous session with the Authentication Authority has been cached by the local system and is now re-used (e.g. a Master Secret is used to derive new session keys in TLS, SSL, WTLS). This element indicates that the Principal has been authenticated by a zero knowledge technique as specified in ISO/IEC 9798-5. This element indicates that the Principal has been authenticated by a challenge-response protocol utilizing shared secret keys and symmetric cryptography. This element indicates that the Principal has been authenticated by a mechanism which involves the Principal computing a digital signature over at least challenge data provided by the IdP. The local system has a private key but it is used in decryption mode, rather than signature mode. For example, the Authentication Authority generates a secret and encrypts it using the local system's public key: the local system then proves it has decrypted the secret. The local system has a private key and uses it for shared secret key agreement with the Authentication Authority (e.g. via Diffie Helman). This element indicates that the Principal has been authenticated through connection from a particular IP address. The local system and Authentication Authority share a secret key. The local system uses this to encrypt a randomised string to pass to the Authentication Authority. The protocol across which Authenticator information is transferred to an Authentication Authority verifier. This element indicates that the Authenticator has been transmitted using bare HTTP utilizing no additional security protocols. This element indicates that the Authenticator has been transmitted using a transport mechanism protected by an IPSEC session. This element indicates that the Authenticator has been transmitted using a transport mechanism protected by a WTLS session. This element indicates that the Authenticator has been transmitted solely across a mobile network using no additional security mechanism. This element indicates that the Authenticator has been transmitted using a transport mechnanism protected by an SSL or TLS session. Refers to those characteristics that describe procedural security controls employed by the Authentication Authority. Provides a mechanism for linking to external (likely human readable) documents in which additional business agreements, (e.g. liability constraints, obligations, etc) can be placed. This attribute indicates whether or not the Identification mechanisms allow the actions of the Principal to be linked to an actual end user. This element indicates that the Key Activation Limit is defined as a specific duration of time. This element indicates that the Key Activation Limit is defined as a number of usages. This element indicates that the Key Activation Limit is the session. ruby-saml-1.15.0/lib/schemas/xml.xsd0000644000004100000410000002120414405647360017262 0ustar www-datawww-data

About the XML namespace

This schema document describes the XML namespace, in a form suitable for import by other schema documents.

See http://www.w3.org/XML/1998/namespace.html and http://www.w3.org/TR/REC-xml for information about this namespace.

Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The names currently defined in this namespace are listed below. They should not be used with conflicting semantics by any Working Group, specification, or document instance.

See further below in this document for more information about how to refer to this schema document from your own XSD schema documents and about the namespace-versioning policy governing this schema document.

lang (as an attribute name)

denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

Notes

Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility.

See BCP 47 at http://www.rfc-editor.org/rfc/bcp/bcp47.txt and the IANA language subtag registry at http://www.iana.org/assignments/language-subtag-registry for further information.

The union allows for the 'un-declaration' of xml:lang with the empty string.

space (as an attribute name)

denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

base (as an attribute name)

denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification.

See http://www.w3.org/TR/xmlbase/ for information about this attribute.

id (as an attribute name)

denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification.

See http://www.w3.org/TR/xml-id/ for information about this attribute.

Father (in any context at all)

denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups:

In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000, reserves for Jon Bosak in perpetuity the XML name "xml:Father".

About this schema document

This schema defines attributes and an attribute group suitable for use by schemas wishing to allow xml:base, xml:lang, xml:space or xml:id attributes on elements they define.

To enable this, such a schema must import this schema for the XML namespace, e.g. as follows:

          <schema . . .>
           . . .
           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
     

or

           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
     

Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g.

          <type . . .>
           . . .
           <attributeGroup ref="xml:specialAttrs"/>
     

will define a type which will schema-validate an instance element with any of those attributes.

Versioning policy for this schema document

In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at http://www.w3.org/2009/01/xml.xsd.

At the date of issue it can also be found at http://www.w3.org/2001/xml.xsd.

The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at http://www.w3.org/2001/xml.xsd will change accordingly; the version at http://www.w3.org/2009/01/xml.xsd will not change.

Previous dated (and unchanging) versions of this schema document are at:

ruby-saml-1.15.0/lib/schemas/saml-schema-assertion-2.0.xsd0000644000004100000410000003074614405647360023171 0ustar www-datawww-data Document identifier: saml-schema-assertion-2.0 Location: http://docs.oasis-open.org/security/saml/v2.0/ Revision history: V1.0 (November, 2002): Initial Standard Schema. V1.1 (September, 2003): Updates within the same V1.0 namespace. V2.0 (March, 2005): New assertion schema for SAML V2.0 namespace. ruby-saml-1.15.0/lib/schemas/xenc-schema.xsd0000644000004100000410000001133614405647360020662 0ustar www-datawww-data ruby-saml-1.15.0/lib/ruby-saml.rb0000644000004100000410000000003514405647360016556 0ustar www-datawww-datarequire 'onelogin/ruby-saml' ruby-saml-1.15.0/lib/onelogin/0000755000004100000410000000000014405647360016132 5ustar www-datawww-dataruby-saml-1.15.0/lib/onelogin/ruby-saml.rb0000644000004100000410000000130614405647360020372 0ustar www-datawww-datarequire 'onelogin/ruby-saml/logging' require 'onelogin/ruby-saml/saml_message' require 'onelogin/ruby-saml/authrequest' require 'onelogin/ruby-saml/logoutrequest' require 'onelogin/ruby-saml/logoutresponse' require 'onelogin/ruby-saml/attributes' require 'onelogin/ruby-saml/slo_logoutrequest' require 'onelogin/ruby-saml/slo_logoutresponse' require 'onelogin/ruby-saml/response' require 'onelogin/ruby-saml/settings' require 'onelogin/ruby-saml/attribute_service' require 'onelogin/ruby-saml/http_error' require 'onelogin/ruby-saml/validation_error' require 'onelogin/ruby-saml/metadata' require 'onelogin/ruby-saml/idp_metadata_parser' require 'onelogin/ruby-saml/utils' require 'onelogin/ruby-saml/version' ruby-saml-1.15.0/lib/onelogin/ruby-saml/0000755000004100000410000000000014405647360020045 5ustar www-datawww-dataruby-saml-1.15.0/lib/onelogin/ruby-saml/version.rb0000644000004100000410000000010314405647360022051 0ustar www-datawww-datamodule OneLogin module RubySaml VERSION = '1.15.0' end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/idp_metadata_parser.rb0000644000004100000410000005337214405647360024374 0ustar www-datawww-datarequire "base64" require "net/http" require "net/https" require "rexml/document" require "rexml/xpath" # Only supports SAML 2.0 module OneLogin module RubySaml include REXML # Auxiliary class to retrieve and parse the Identity Provider Metadata # # This class does not validate in any way the URL that is introduced, # make sure to validate it properly before use it in a parse_remote method. # Read the `Security warning` section of the README.md file to get more info # class IdpMetadataParser module SamlMetadata module Vocabulary METADATA = "urn:oasis:names:tc:SAML:2.0:metadata".freeze DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*".freeze SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion".freeze end NAMESPACE = { "md" => Vocabulary::METADATA, "NameFormat" => Vocabulary::NAME_FORMAT, "saml" => Vocabulary::SAML_ASSERTION, "ds" => Vocabulary::DSIG }.freeze end include SamlMetadata::Vocabulary attr_reader :document attr_reader :response attr_reader :options # fetch IdP descriptors from a metadata document def self.get_idps(metadata_document, only_entity_id=nil) path = "//md:EntityDescriptor#{only_entity_id && '[@entityID="' + only_entity_id + '"]'}/md:IDPSSODescriptor" REXML::XPath.match( metadata_document, path, SamlMetadata::NAMESPACE ) end # Parse the Identity Provider metadata and update the settings with the # IdP values # # @param url [String] Url where the XML of the Identity Provider Metadata is published. # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. # # @param options [Hash] options used for parsing the metadata and the returned Settings instance # @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides. # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. # # @return [OneLogin::RubySaml::Settings] # # @raise [HttpError] Failure to fetch remote IdP metadata def parse_remote(url, validate_cert = true, options = {}) idp_metadata = get_idp_metadata(url, validate_cert) parse(idp_metadata, options) end # Parse the Identity Provider metadata and return the results as Hash # # @param url [String] Url where the XML of the Identity Provider Metadata is published. # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. # # @param options [Hash] options used for parsing the metadata # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. # # @return [Hash] # # @raise [HttpError] Failure to fetch remote IdP metadata def parse_remote_to_hash(url, validate_cert = true, options = {}) parse_remote_to_array(url, validate_cert, options)[0] end # Parse all Identity Provider metadata and return the results as Array # # @param url [String] Url where the XML of the Identity Provider Metadata is published. # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. # # @param options [Hash] options used for parsing the metadata # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned. # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. # # @return [Array] # # @raise [HttpError] Failure to fetch remote IdP metadata def parse_remote_to_array(url, validate_cert = true, options = {}) idp_metadata = get_idp_metadata(url, validate_cert) parse_to_array(idp_metadata, options) end # Parse the Identity Provider metadata and update the settings with the IdP values # # @param idp_metadata [String] # # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides # @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides. # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. # # @return [OneLogin::RubySaml::Settings] def parse(idp_metadata, options = {}) parsed_metadata = parse_to_hash(idp_metadata, options) unless parsed_metadata[:cache_duration].nil? cache_valid_until_timestamp = OneLogin::RubySaml::Utils.parse_duration(parsed_metadata[:cache_duration]) unless cache_valid_until_timestamp.nil? if parsed_metadata[:valid_until].nil? || cache_valid_until_timestamp < Time.parse(parsed_metadata[:valid_until], Time.now.utc).to_i parsed_metadata[:valid_until] = Time.at(cache_valid_until_timestamp).utc.strftime("%Y-%m-%dT%H:%M:%SZ") end end end # Remove the cache_duration because on the settings # we only gonna suppot valid_until parsed_metadata.delete(:cache_duration) settings = options[:settings] if settings.nil? OneLogin::RubySaml::Settings.new(parsed_metadata) elsif settings.is_a?(Hash) OneLogin::RubySaml::Settings.new(settings.merge(parsed_metadata)) else merge_parsed_metadata_into(settings, parsed_metadata) end end # Parse the Identity Provider metadata and return the results as Hash # # @param idp_metadata [String] # # @param options [Hash] options used for parsing the metadata and the returned Settings instance # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used. # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. # # @return [Hash] def parse_to_hash(idp_metadata, options = {}) parse_to_array(idp_metadata, options)[0] end # Parse all Identity Provider metadata and return the results as Array # # @param idp_metadata [String] # # @param options [Hash] options used for parsing the metadata and the returned Settings instance # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned. # @option options [String, Array, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used. # @option options [String, Array, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used. # # @return [Array] def parse_to_array(idp_metadata, options = {}) parse_to_idp_metadata_array(idp_metadata, options).map { |idp_md| idp_md.to_hash(options) } end def parse_to_idp_metadata_array(idp_metadata, options = {}) @document = REXML::Document.new(idp_metadata) @options = options idpsso_descriptors = self.class.get_idps(@document, options[:entity_id]) if !idpsso_descriptors.any? raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element") end idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])} end private # Retrieve the remote IdP metadata from the URL or a cached copy. # @param url [String] Url where the XML of the Identity Provider Metadata is published. # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked. # @return [REXML::document] Parsed XML IdP metadata # @raise [HttpError] Failure to fetch remote IdP metadata def get_idp_metadata(url, validate_cert) uri = URI.parse(url) raise ArgumentError.new("url must begin with http or https") unless /^https?/ =~ uri.scheme http = Net::HTTP.new(uri.host, uri.port) if uri.scheme == "https" http.use_ssl = true # Most IdPs will probably use self signed certs http.verify_mode = validate_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE # Net::HTTP in Ruby 1.8 did not set the default certificate store # automatically when VERIFY_PEER was specified. if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE end end get = Net::HTTP::Get.new(uri.request_uri) get.basic_auth uri.user, uri.password if uri.user @response = http.request(get) return response.body if response.is_a? Net::HTTPSuccess raise OneLogin::RubySaml::HttpError.new( "Failed to fetch idp metadata: #{response.code}: #{response.message}" ) end class IdpMetadata attr_reader :idpsso_descriptor, :entity_id def initialize(idpsso_descriptor, entity_id) @idpsso_descriptor = idpsso_descriptor @entity_id = entity_id end def to_hash(options = {}) sso_binding = options[:sso_binding] slo_binding = options[:slo_binding] { :idp_entity_id => @entity_id, :name_identifier_format => idp_name_id_format(options[:name_id_format]), :idp_sso_service_url => single_signon_service_url(sso_binding), :idp_sso_service_binding => single_signon_service_binding(sso_binding), :idp_slo_service_url => single_logout_service_url(slo_binding), :idp_slo_service_binding => single_logout_service_binding(slo_binding), :idp_slo_response_service_url => single_logout_response_service_url(slo_binding), :idp_attribute_names => attribute_names, :idp_cert => nil, :idp_cert_fingerprint => nil, :idp_cert_multi => nil, :valid_until => valid_until, :cache_duration => cache_duration, }.tap do |response_hash| merge_certificates_into(response_hash) unless certificates.nil? end end # @return [String|nil] 'validUntil' attribute of metadata # def valid_until root = @idpsso_descriptor.root root.attributes['validUntil'] if root && root.attributes end # @return [String|nil] 'cacheDuration' attribute of metadata # def cache_duration root = @idpsso_descriptor.root root.attributes['cacheDuration'] if root && root.attributes end # @param name_id_priority [String|Array] The prioritized list of NameIDFormat values to select. Will select first value if nil. # @return [String|nil] IdP NameIDFormat value if exists # def idp_name_id_format(name_id_priority = nil) nodes = REXML::XPath.match( @idpsso_descriptor, "md:NameIDFormat", SamlMetadata::NAMESPACE ) first_ranked_text(nodes, name_id_priority) end # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. # @return [String|nil] SingleSignOnService binding if exists # def single_signon_service_binding(binding_priority = nil) nodes = REXML::XPath.match( @idpsso_descriptor, "md:SingleSignOnService/@Binding", SamlMetadata::NAMESPACE ) first_ranked_value(nodes, binding_priority) end # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. # @return [String|nil] SingleLogoutService binding if exists # def single_logout_service_binding(binding_priority = nil) nodes = REXML::XPath.match( @idpsso_descriptor, "md:SingleLogoutService/@Binding", SamlMetadata::NAMESPACE ) first_ranked_value(nodes, binding_priority) end # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. # @return [String|nil] SingleSignOnService endpoint if exists # def single_signon_service_url(binding_priority = nil) binding = single_signon_service_binding(binding_priority) return if binding.nil? node = REXML::XPath.first( @idpsso_descriptor, "md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location", SamlMetadata::NAMESPACE ) node.value if node end # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. # @return [String|nil] SingleLogoutService endpoint if exists # def single_logout_service_url(binding_priority = nil) binding = single_logout_service_binding(binding_priority) return if binding.nil? node = REXML::XPath.first( @idpsso_descriptor, "md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location", SamlMetadata::NAMESPACE ) node.value if node end # @param binding_priority [String|Array] The prioritized list of Binding values to select. Will select first value if nil. # @return [String|nil] SingleLogoutService response url if exists # def single_logout_response_service_url(binding_priority = nil) binding = single_logout_service_binding(binding_priority) return if binding.nil? node = REXML::XPath.first( @idpsso_descriptor, "md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation", SamlMetadata::NAMESPACE ) node.value if node end # @return [String|nil] Unformatted Certificate if exists # def certificates @certificates ||= begin signing_nodes = REXML::XPath.match( @idpsso_descriptor, "md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate", SamlMetadata::NAMESPACE ) encryption_nodes = REXML::XPath.match( @idpsso_descriptor, "md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate", SamlMetadata::NAMESPACE ) return nil if signing_nodes.empty? && encryption_nodes.empty? certs = {} unless signing_nodes.empty? certs['signing'] = [] signing_nodes.each do |cert_node| certs['signing'] << Utils.element_text(cert_node) end end unless encryption_nodes.empty? certs['encryption'] = [] encryption_nodes.each do |cert_node| certs['encryption'] << Utils.element_text(cert_node) end end certs end end # @return [String|nil] the fingerpint of the X509Certificate if it exists # def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1) @fingerprint ||= begin return unless certificate cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate)) fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":") end end # @return [Array] the names of all SAML attributes if any exist # def attribute_names nodes = REXML::XPath.match( @idpsso_descriptor , "saml:Attribute/@Name", SamlMetadata::NAMESPACE ) nodes.map(&:value) end def merge_certificates_into(parsed_metadata) if (certificates.size == 1 && (certificates_has_one('signing') || certificates_has_one('encryption'))) || (certificates_has_one('signing') && certificates_has_one('encryption') && certificates["signing"][0] == certificates["encryption"][0]) if certificates.key?("signing") parsed_metadata[:idp_cert] = certificates["signing"][0] parsed_metadata[:idp_cert_fingerprint] = fingerprint( parsed_metadata[:idp_cert], parsed_metadata[:idp_cert_fingerprint_algorithm] ) else parsed_metadata[:idp_cert] = certificates["encryption"][0] parsed_metadata[:idp_cert_fingerprint] = fingerprint( parsed_metadata[:idp_cert], parsed_metadata[:idp_cert_fingerprint_algorithm] ) end end # symbolize keys of certificates and pass it on parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }] end def certificates_has_one(key) certificates.key?(key) && certificates[key].size == 1 end private def first_ranked_text(nodes, priority = nil) return unless nodes.any? priority = Array(priority) if priority.any? values = nodes.map(&:text) priority.detect { |candidate| values.include?(candidate) } else nodes.first.text end end def first_ranked_value(nodes, priority = nil) return unless nodes.any? priority = Array(priority) if priority.any? values = nodes.map(&:value) priority.detect { |candidate| values.include?(candidate) } else nodes.first.value end end end def merge_parsed_metadata_into(settings, parsed_metadata) parsed_metadata.each do |key, value| settings.send("#{key}=".to_sym, value) end settings end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/logging.rb0000644000004100000410000000122114405647360022014 0ustar www-datawww-datarequire 'logger' # Simplistic log class when we're running in Rails module OneLogin module RubySaml class Logging DEFAULT_LOGGER = ::Logger.new(STDOUT) def self.logger @logger ||= begin (defined?(::Rails) && Rails.respond_to?(:logger) && Rails.logger) || DEFAULT_LOGGER end end def self.logger=(logger) @logger = logger end def self.debug(message) return if !!ENV["ruby-saml/testing"] logger.debug message end def self.info(message) return if !!ENV["ruby-saml/testing"] logger.info message end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/settings.rb0000644000004100000410000002330314405647360022233 0ustar www-datawww-datarequire "xml_security" require "onelogin/ruby-saml/attribute_service" require "onelogin/ruby-saml/utils" require "onelogin/ruby-saml/validation_error" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Toolkit Settings # class Settings def initialize(overrides = {}, keep_security_attributes = false) if keep_security_attributes security_attributes = overrides.delete(:security) || {} config = DEFAULTS.merge(overrides) config[:security] = DEFAULTS[:security].merge(security_attributes) else config = DEFAULTS.merge(overrides) end config.each do |k,v| acc = "#{k}=".to_sym if respond_to? acc value = v.is_a?(Hash) ? v.dup : v send(acc, value) end end @attribute_consuming_service = AttributeService.new end # IdP Data attr_accessor :idp_entity_id attr_writer :idp_sso_service_url attr_writer :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_accessor :idp_cert_multi attr_accessor :idp_attribute_names attr_accessor :idp_name_qualifier attr_accessor :valid_until # SP Data attr_writer :sp_entity_id attr_accessor :assertion_consumer_service_url attr_reader :assertion_consumer_service_binding attr_writer :single_logout_service_url attr_accessor :sp_name_qualifier attr_accessor :name_identifier_format attr_accessor :name_identifier_value attr_accessor :name_identifier_value_requested attr_accessor :sessionindex attr_accessor :compress_request attr_accessor :compress_response attr_accessor :double_quote_xml_attribute_values attr_accessor :message_max_bytesize attr_accessor :passive attr_reader :protocol_binding attr_accessor :attributes_index attr_accessor :force_authn attr_accessor :certificate attr_accessor :certificate_new attr_accessor :private_key attr_accessor :authn_context attr_accessor :authn_context_comparison attr_accessor :authn_context_decl_ref attr_reader :attribute_consuming_service # Work-flow attr_accessor :security attr_accessor :soft # Deprecated attr_accessor :assertion_consumer_logout_service_url attr_reader :assertion_consumer_logout_service_binding attr_accessor :issuer attr_accessor :idp_sso_target_url attr_accessor :idp_slo_target_url # @return [String] IdP Single Sign On Service URL # def idp_sso_service_url @idp_sso_service_url || @idp_sso_target_url end # @return [String] IdP Single Logout Service URL # def idp_slo_service_url @idp_slo_service_url || @idp_slo_target_url end # @return [String] IdP Single Sign On Service Binding # def idp_sso_service_binding @idp_sso_service_binding || idp_binding_from_embed_sign end # Setter for IdP Single Sign On Service Binding # @param value [String, Symbol]. # def idp_sso_service_binding=(value) @idp_sso_service_binding = get_binding(value) end # @return [String] IdP Single Logout Service Binding # def idp_slo_service_binding @idp_slo_service_binding || idp_binding_from_embed_sign end # Setter for IdP Single Logout Service Binding # @param value [String, Symbol]. # def idp_slo_service_binding=(value) @idp_slo_service_binding = get_binding(value) end # @return [String] SP Entity ID # def sp_entity_id @sp_entity_id || @issuer end # Setter for SP Protocol Binding # @param value [String, Symbol]. # def protocol_binding=(value) @protocol_binding = get_binding(value) end # Setter for SP Assertion Consumer Service Binding # @param value [String, Symbol]. # def assertion_consumer_service_binding=(value) @assertion_consumer_service_binding = get_binding(value) end # @return [String] Single Logout Service URL. # def single_logout_service_url @single_logout_service_url || @assertion_consumer_logout_service_url end # @return [String] Single Logout Service Binding. # def single_logout_service_binding @single_logout_service_binding || @assertion_consumer_logout_service_binding end # Setter for Single Logout Service Binding. # # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect") # @param value [String, Symbol] # def single_logout_service_binding=(value) @single_logout_service_binding = get_binding(value) end # @deprecated Setter for legacy Single Logout Service Binding parameter. # # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect") # @param value [String, Symbol] # def assertion_consumer_logout_service_binding=(value) @assertion_consumer_logout_service_binding = get_binding(value) end # Calculates the fingerprint of the IdP x509 certificate. # @return [String] The fingerprint # def get_fingerprint idp_cert_fingerprint || begin idp_cert = get_idp_cert if idp_cert fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":") end end end # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it) # def get_idp_cert return nil if idp_cert.nil? || idp_cert.empty? formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert) OpenSSL::X509::Certificate.new(formatted_cert) end # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings. # def get_idp_cert_multi return nil if idp_cert_multi.nil? || idp_cert_multi.empty? raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash) certs = {:signing => [], :encryption => [] } [:signing, :encryption].each do |type| certs_for_type = idp_cert_multi[type] || idp_cert_multi[type.to_s] next if !certs_for_type || certs_for_type.empty? certs_for_type.each do |idp_cert| formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert) certs[type].push(OpenSSL::X509::Certificate.new(formatted_cert)) end end certs end # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it) # def get_sp_cert return nil if certificate.nil? || certificate.empty? formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate) cert = OpenSSL::X509::Certificate.new(formatted_cert) if security[:check_sp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(cert) raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.") end end cert end # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it) # def get_sp_cert_new return nil if certificate_new.nil? || certificate_new.empty? formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new) OpenSSL::X509::Certificate.new(formatted_cert) end # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it) # def get_sp_key return nil if private_key.nil? || private_key.empty? formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key) OpenSSL::PKey::RSA.new(formatted_private_key) end def idp_binding_from_embed_sign security[:embed_sign] ? Utils::BINDINGS[:post] : Utils::BINDINGS[:redirect] end def get_binding(value) return unless value Utils::BINDINGS[value.to_sym] || value end DEFAULTS = { :assertion_consumer_service_binding => Utils::BINDINGS[:post], :single_logout_service_binding => Utils::BINDINGS[:redirect], :idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1, :compress_request => true, :compress_response => true, :message_max_bytesize => 250000, :soft => true, :double_quote_xml_attribute_values => false, :security => { :authn_requests_signed => false, :logout_requests_signed => false, :logout_responses_signed => false, :want_assertions_signed => false, :want_assertions_encrypted => false, :want_name_id => false, :metadata_signed => false, :embed_sign => false, # Deprecated :digest_method => XMLSecurity::Document::SHA1, :signature_method => XMLSecurity::Document::RSA_SHA1, :check_idp_cert_expiration => false, :check_sp_cert_expiration => false, :strict_audience_validation => false, :lowercase_url_encoding => false }.freeze }.freeze end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/validation_error.rb0000644000004100000410000000013714405647360023736 0ustar www-datawww-datamodule OneLogin module RubySaml class ValidationError < StandardError end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/slo_logoutresponse.rb0000644000004100000410000001632014405647360024341 0ustar www-datawww-datarequire "onelogin/ruby-saml/logging" require "onelogin/ruby-saml/saml_message" require "onelogin/ruby-saml/utils" require "onelogin/ruby-saml/setting_error" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Logout Response (SLO SP initiated, Parser) # class SloLogoutresponse < SamlMessage # Logout Response ID attr_accessor :uuid # Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class. # Asigns an ID, a random uuid. # def initialize @uuid = OneLogin::RubySaml::Utils.uuid end def response_id @uuid end # Creates the Logout Response string. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response # @param logout_message [String] The Message to be placed as StatusMessage in the logout response # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response # @return [String] Logout Request string that includes the SAMLRequest # def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil) params = create_params(settings, request_id, logout_message, params, logout_status_code) params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?' url = settings.idp_slo_response_service_url || settings.idp_slo_service_url saml_response = CGI.escape(params.delete("SAMLResponse")) response_params = "#{params_prefix}SAMLResponse=#{saml_response}" params.each_pair do |key, value| response_params << "&#{key}=#{CGI.escape(value.to_s)}" end raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty? @logout_url = url + response_params end # Creates the Get parameters for the logout response. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response # @param logout_message [String] The Message to be placed as StatusMessage in the logout response # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response # @return [Hash] Parameters # def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil) # The method expects :RelayState but sometimes we get 'RelayState' instead. # Based on the HashWithIndifferentAccess value in Rails we could experience # conflicts so this line will solve them. relay_state = params[:RelayState] || params['RelayState'] if relay_state.nil? params.delete(:RelayState) params.delete('RelayState') end response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code) response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values response = "" response_doc.write(response) Logging.debug "Created SLO Logout Response: #{response}" response = deflate(response) if settings.compress_response base64_response = encode(response) response_params = {"SAMLResponse" => base64_response} if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && settings.private_key params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( :type => 'SAMLResponse', :data => base64_response, :relay_state => relay_state, :sig_alg => params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end params.each_pair do |key, value| response_params[key] = value.to_s end response_params end # Creates the SAMLResponse String. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response # @param logout_message [String] The Message to be placed as StatusMessage in the logout response # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response # @return [String] The SAMLResponse String. # def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil) document = create_xml_document(settings, request_id, logout_message, logout_status_code) sign_document(document, settings) end def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil) time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ') response_doc = XMLSecurity::Document.new response_doc.uuid = uuid destination = settings.idp_slo_response_service_url || settings.idp_slo_service_url root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } root.attributes['ID'] = uuid root.attributes['IssueInstant'] = time root.attributes['Version'] = '2.0' root.attributes['InResponseTo'] = request_id unless request_id.nil? root.attributes['Destination'] = destination unless destination.nil? or destination.empty? if settings.sp_entity_id != nil issuer = root.add_element "saml:Issuer" issuer.text = settings.sp_entity_id end # add status status = root.add_element 'samlp:Status' # status code status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success' status_code_elem = status.add_element 'samlp:StatusCode' status_code_elem.attributes['Value'] = status_code # status message logout_message ||= 'Successfully Signed Out' status_message = status.add_element 'samlp:StatusMessage' status_message.text = logout_message response_doc end def sign_document(document, settings) # embed signature if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.private_key && settings.certificate private_key = settings.get_sp_key cert = settings.get_sp_cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end document end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/slo_logoutrequest.rb0000644000004100000410000002524314405647360024177 0ustar www-datawww-datarequire 'zlib' require 'time' require 'nokogiri' require "onelogin/ruby-saml/saml_message" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Logout Request (SLO IdP initiated, Parser) # class SloLogoutrequest < SamlMessage include ErrorHandling # OneLogin::RubySaml::Settings Toolkit settings attr_accessor :settings attr_reader :document attr_reader :request attr_reader :options attr_accessor :soft # Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class. # @param request [String] A UUEncoded Logout Request from the IdP. # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object # Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with # Or :relax_signature_validation to accept signatures if no idp certificate registered on settings # # @raise [ArgumentError] If Request is nil # def initialize(request, options = {}) raise ArgumentError.new("Request cannot be nil") if request.nil? @errors = [] @options = options @soft = true unless options[:settings].nil? @settings = options[:settings] unless @settings.soft.nil? @soft = @settings.soft end end @request = decode_raw_saml(request, settings) @document = REXML::Document.new(@request) end def request_id id(document) end # Validates the Logout Request with the default values (soft = true) # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. # @return [Boolean] TRUE if the Logout Request is valid # def is_valid?(collect_errors = false) validate(collect_errors) end # @return [String] Gets the NameID of the Logout Request. # def name_id @name_id ||= begin node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) Utils.element_text(node) end end alias_method :nameid, :name_id # @return [String] Gets the NameID Format of the Logout Request. # def name_id_format @name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) @name_id_format ||= if @name_id_node && @name_id_node.attribute("Format") @name_id_node.attribute("Format").value end end alias_method :nameid_format, :name_id_format # @return [String|nil] Gets the ID attribute from the Logout Request. if exists. # def id super(document) end # @return [String] Gets the Issuer from the Logout Request. # def issuer @issuer ||= begin node = REXML::XPath.first( document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION } ) Utils.element_text(node) end end # @return [Time|nil] Gets the NotOnOrAfter Attribute value if exists. # def not_on_or_after @not_on_or_after ||= begin node = REXML::XPath.first( document, "/p:LogoutRequest", { "p" => PROTOCOL } ) if node && node.attributes["NotOnOrAfter"] Time.parse(node.attributes["NotOnOrAfter"]) end end end # @return [Array] Gets the SessionIndex if exists (Supported multiple values). Empty Array if none found # def session_indexes nodes = REXML::XPath.match( document, "/p:LogoutRequest/p:SessionIndex", { "p" => PROTOCOL } ) nodes.map { |node| Utils.element_text(node) } end private # returns the allowed clock drift on timing validation # @return [Float] def allowed_clock_drift options[:allowed_clock_drift].to_f.abs + Float::EPSILON end # Hard aux function to validate the Logout Request # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) # @return [Boolean] TRUE if the Logout Request is valid # @raise [ValidationError] if soft == false and validation fails # def validate(collect_errors = false) reset_errors! validations = [ :validate_request_state, :validate_id, :validate_version, :validate_structure, :validate_not_on_or_after, :validate_issuer, :validate_signature ] if collect_errors validations.each { |validation| send(validation) } @errors.empty? else validations.all? { |validation| send(validation) } end end # Validates that the Logout Request contains an ID # If fails, the error is added to the errors array. # @return [Boolean] True if the Logout Request contains an ID, otherwise returns False # def validate_id unless id return append_error("Missing ID attribute on Logout Request") end true end # Validates the SAML version (2.0) # If fails, the error is added to the errors array. # @return [Boolean] True if the Logout Request is 2.0, otherwise returns False # def validate_version unless version(document) == "2.0" return append_error("Unsupported SAML version") end true end # Validates the time. (If the logout request was initialized with the :allowed_clock_drift # option, the timing validations are relaxed by the allowed_clock_drift value) # If fails, the error is added to the errors array # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_not_on_or_after now = Time.now.utc if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})") end true end # Validates the Logout Request against the specified schema. # @return [Boolean] True if the XML is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_structure unless valid_saml?(document, soft) return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd") end true end # Validates that the Logout Request provided in the initialization is not empty, # @return [Boolean] True if the required info is found, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_request_state return append_error("Blank logout request") if request.nil? || request.empty? true end # Validates the Issuer of the Logout Request # If fails, the error is added to the errors array # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_issuer return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil? unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") end true end # Validates the Signature if exists and GET parameters are provided # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_signature return true if options.nil? return true unless options.has_key? :get_params return true unless options[:get_params].has_key? 'Signature' options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding]) if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil? options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg']) end idp_cert = settings.get_idp_cert idp_certs = settings.get_idp_cert_multi if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) return options.has_key? :relax_signature_validation end query_string = OneLogin::RubySaml::Utils.build_query_from_raw_parts( :type => 'SAMLRequest', :raw_data => options[:raw_get_params]['SAMLRequest'], :raw_relay_state => options[:raw_get_params]['RelayState'], :raw_sig_alg => options[:raw_get_params]['SigAlg'] ) expired = false if idp_certs.nil? || idp_certs[:signing].empty? valid = OneLogin::RubySaml::Utils.verify_signature( :cert => idp_cert, :sig_alg => options[:get_params]['SigAlg'], :signature => options[:get_params]['Signature'], :query_string => query_string ) if valid && settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) expired = true end end else valid = false idp_certs[:signing].each do |signing_idp_cert| valid = OneLogin::RubySaml::Utils.verify_signature( :cert => signing_idp_cert, :sig_alg => options[:get_params]['SigAlg'], :signature => options[:get_params]['Signature'], :query_string => query_string ) if valid if settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert) expired = true end end break end end end if expired error_msg = "IdP x509 certificate expired" return append_error(error_msg) end unless valid return append_error("Invalid Signature on Logout Request") end true end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/setting_error.rb0000644000004100000410000000013214405647360023254 0ustar www-datawww-datamodule OneLogin module RubySaml class SettingError < StandardError end end endruby-saml-1.15.0/lib/onelogin/ruby-saml/metadata.rb0000644000004100000410000001534714405647360022164 0ustar www-datawww-datarequire "uri" require "onelogin/ruby-saml/logging" require "onelogin/ruby-saml/utils" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Metadata. XML Metadata Builder # class Metadata # Return SP metadata based on the settings. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param pretty_print [Boolean] Pretty print or not the response # (No pretty print if you gonna validate the signature) # @param valid_until [DateTime] Metadata's valid time # @param cache_duration [Integer] Duration of the cache in seconds # @return [String] XML Metadata of the Service Provider # def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil) meta_doc = XMLSecurity::Document.new add_xml_declaration(meta_doc) root = add_root_element(meta_doc, settings, valid_until, cache_duration) sp_sso = add_sp_sso_element(root, settings) add_sp_certificates(sp_sso, settings) add_sp_service_elements(sp_sso, settings) add_extras(root, settings) embed_signature(meta_doc, settings) output_xml(meta_doc, pretty_print) end protected def add_xml_declaration(meta_doc) meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8') end def add_root_element(meta_doc, settings, valid_until, cache_duration) namespaces = { "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata" } if settings.attribute_consuming_service.configured? namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion" end root = meta_doc.add_element("md:EntityDescriptor", namespaces) root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id root.attributes["validUntil"] = valid_until.utc.strftime('%Y-%m-%dT%H:%M:%SZ') if valid_until root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration root end def add_sp_sso_element(root, settings) root.add_element "md:SPSSODescriptor", { "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequestsSigned" => settings.security[:authn_requests_signed], "WantAssertionsSigned" => settings.security[:want_assertions_signed], } end # Add KeyDescriptor if messages will be signed / encrypted # with SP certificate, and new SP certificate if any def add_sp_certificates(sp_sso, settings) cert = settings.get_sp_cert cert_new = settings.get_sp_cert_new for sp_cert in [cert, cert_new] if sp_cert cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '') kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd = ki.add_element "ds:X509Data" xc = xd.add_element "ds:X509Certificate" xc.text = cert_text if settings.security[:want_assertions_encrypted] kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" } ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd2 = ki2.add_element "ds:X509Data" xc2 = xd2.add_element "ds:X509Certificate" xc2.text = cert_text end end end sp_sso end def add_sp_service_elements(sp_sso, settings) if settings.single_logout_service_url sp_sso.add_element "md:SingleLogoutService", { "Binding" => settings.single_logout_service_binding, "Location" => settings.single_logout_service_url, "ResponseLocation" => settings.single_logout_service_url } end if settings.name_identifier_format nameid = sp_sso.add_element "md:NameIDFormat" nameid.text = settings.name_identifier_format end if settings.assertion_consumer_service_url sp_sso.add_element "md:AssertionConsumerService", { "Binding" => settings.assertion_consumer_service_binding, "Location" => settings.assertion_consumer_service_url, "isDefault" => true, "index" => 0 } end if settings.attribute_consuming_service.configured? sp_acs = sp_sso.add_element "md:AttributeConsumingService", { "isDefault" => "true", "index" => settings.attribute_consuming_service.index } srv_name = sp_acs.add_element "md:ServiceName", { "xml:lang" => "en" } srv_name.text = settings.attribute_consuming_service.name settings.attribute_consuming_service.attributes.each do |attribute| sp_req_attr = sp_acs.add_element "md:RequestedAttribute", { "NameFormat" => attribute[:name_format], "Name" => attribute[:name], "FriendlyName" => attribute[:friendly_name], "isRequired" => attribute[:is_required] || false } unless attribute[:attribute_value].nil? Array(attribute[:attribute_value]).each do |value| sp_attr_val = sp_req_attr.add_element "saml:AttributeValue" sp_attr_val.text = value.to_s end end end end # With OpenSSO, it might be required to also include # # sp_sso end # can be overridden in subclass def add_extras(root, _settings) root end def embed_signature(meta_doc, settings) return unless settings.security[:metadata_signed] private_key = settings.get_sp_key cert = settings.get_sp_cert return unless private_key && cert meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end def output_xml(meta_doc, pretty_print) ret = '' # pretty print the XML so IdP administrators can easily see what the SP supports if pretty_print meta_doc.write(ret, 1) else ret = meta_doc.to_s end ret end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/attributes.rb0000644000004100000410000001140014405647360022554 0ustar www-datawww-datamodule OneLogin module RubySaml # SAML2 Attributes. Parse the Attributes from the AttributeStatement of the SAML Response. # class Attributes include Enumerable attr_reader :attributes # By default Attributes#[] is backwards compatible and # returns only the first value for the attribute # Setting this to `false` returns all values for an attribute @@single_value_compatibility = true # @return [Boolean] Get current status of backwards compatibility mode. # def self.single_value_compatibility @@single_value_compatibility end # Sets the backwards compatibility mode on/off. # @param value [Boolean] # def self.single_value_compatibility=(value) @@single_value_compatibility = value end # @param attrs [Hash] The +attrs+ must be a Hash with attribute names as keys and **arrays** as values: # Attributes.new({ # 'name' => ['value1', 'value2'], # 'mail' => ['value1'], # }) # def initialize(attrs = {}) @attributes = attrs end # Iterate over all attributes # def each attributes.each{|name, values| yield name, values} end # Test attribute presence by name # @param name [String] The attribute name to be checked # def include?(name) attributes.has_key?(canonize_name(name)) end # Return first value for an attribute # @param name [String] The attribute name # @return [String] The value (First occurrence) # def single(name) attributes[canonize_name(name)].first if include?(name) end # Return all values for an attribute # @param name [String] The attribute name # @return [Array] Values of the attribute # def multi(name) attributes[canonize_name(name)] end # Retrieve attribute value(s) # @param name [String] The attribute name # @return [String|Array] Depending on the single value compatibility status this returns: # - First value if single_value_compatibility = true # response.attributes['mail'] # => 'user@example.com' # - All values if single_value_compatibility = false # response.attributes['mail'] # => ['user@example.com','user@example.net'] # def [](name) self.class.single_value_compatibility ? single(canonize_name(name)) : multi(canonize_name(name)) end # @return [Hash] Return all attributes as a hash # def all attributes end # @param name [String] The attribute name # @param values [Array] The values # def set(name, values) attributes[canonize_name(name)] = values end alias_method :[]=, :set # @param name [String] The attribute name # @param values [Array] The values # def add(name, values = []) attributes[canonize_name(name)] ||= [] attributes[canonize_name(name)] += Array(values) end # Make comparable to another Attributes collection based on attributes # @param other [Attributes] An Attributes object to compare with # @return [Boolean] True if are contains the same attributes and values # def ==(other) if other.is_a?(Attributes) all == other.all else super end end # Fetch attribute value using name or regex # @param name [String|Regexp] The attribute name # @return [String|Array] Depending on the single value compatibility status this returns: # - First value if single_value_compatibility = true # response.attributes['mail'] # => 'user@example.com' # - All values if single_value_compatibility = false # response.attributes['mail'] # => ['user@example.com','user@example.net'] # def fetch(name) attributes.each_key do |attribute_key| if name.is_a?(Regexp) if name.respond_to? :match? return self[attribute_key] if name.match?(attribute_key) else return self[attribute_key] if name.match(attribute_key) end elsif canonize_name(name) == canonize_name(attribute_key) return self[attribute_key] end end nil end protected # stringifies all names so both 'email' and :email return the same result # @param name [String] The attribute name # @return [String] stringified name # def canonize_name(name) name.to_s end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/logoutrequest.rb0000644000004100000410000001372714405647360023326 0ustar www-datawww-datarequire "onelogin/ruby-saml/logging" require "onelogin/ruby-saml/saml_message" require "onelogin/ruby-saml/utils" require "onelogin/ruby-saml/setting_error" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Logout Request (SLO SP initiated, Builder) # class Logoutrequest < SamlMessage # Logout Request ID attr_accessor :uuid # Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class. # Asigns an ID, a random uuid. # def initialize @uuid = OneLogin::RubySaml::Utils.uuid end def request_id @uuid end # Creates the Logout Request string. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState # @return [String] Logout Request string that includes the SAMLRequest # def create(settings, params={}) params = create_params(settings, params) params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?' saml_request = CGI.escape(params.delete("SAMLRequest")) request_params = "#{params_prefix}SAMLRequest=#{saml_request}" params.each_pair do |key, value| request_params << "&#{key}=#{CGI.escape(value.to_s)}" end raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty? @logout_url = settings.idp_slo_service_url + request_params end # Creates the Get parameters for the logout request. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState # @return [Hash] Parameters # def create_params(settings, params={}) # The method expects :RelayState but sometimes we get 'RelayState' instead. # Based on the HashWithIndifferentAccess value in Rails we could experience # conflicts so this line will solve them. relay_state = params[:RelayState] || params['RelayState'] if relay_state.nil? params.delete(:RelayState) params.delete('RelayState') end request_doc = create_logout_request_xml_doc(settings) request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values request = "" request_doc.write(request) Logging.debug "Created SLO Logout Request: #{request}" request = deflate(request) if settings.compress_request base64_request = encode(request) request_params = {"SAMLRequest" => base64_request} if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( :type => 'SAMLRequest', :data => base64_request, :relay_state => relay_state, :sig_alg => params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end params.each_pair do |key, value| request_params[key] = value.to_s end request_params end # Creates the SAMLRequest String. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @return [String] The SAMLRequest String. # def create_logout_request_xml_doc(settings) document = create_xml_document(settings) sign_document(document, settings) end def create_xml_document(settings) time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") request_doc = XMLSecurity::Document.new request_doc.uuid = uuid root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } root.attributes['ID'] = uuid root.attributes['IssueInstant'] = time root.attributes['Version'] = "2.0" root.attributes['Destination'] = settings.idp_slo_service_url unless settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty? if settings.sp_entity_id issuer = root.add_element "saml:Issuer" issuer.text = settings.sp_entity_id end nameid = root.add_element "saml:NameID" if settings.name_identifier_value nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format nameid.text = settings.name_identifier_value else # If no NameID is present in the settings we generate one nameid.text = OneLogin::RubySaml::Utils.uuid nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' end if settings.sessionindex sessionindex = root.add_element "samlp:SessionIndex" sessionindex.text = settings.sessionindex end request_doc end def sign_document(document, settings) # embed signature if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate private_key = settings.get_sp_key cert = settings.get_sp_cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end document end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/http_error.rb0000644000004100000410000000013114405647360022555 0ustar www-datawww-datamodule OneLogin module RubySaml class HttpError < StandardError end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/utils.rb0000644000004100000410000004003014405647360021527 0ustar www-datawww-dataif RUBY_VERSION < '1.9' require 'uuid' else require 'securerandom' end require "openssl" module OneLogin module RubySaml # SAML2 Auxiliary class # class Utils @@uuid_generator = UUID.new if RUBY_VERSION < '1.9' BINDINGS = { :post => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze, :redirect => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze }.freeze DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze XENC = "http://www.w3.org/2001/04/xmlenc#".freeze DURATION_FORMAT = %r(^ (-?)P # 1: Duration sign (?: (?:(\d+)Y)? # 2: Years (?:(\d+)M)? # 3: Months (?:(\d+)D)? # 4: Days (?:T (?:(\d+)H)? # 5: Hours (?:(\d+)M)? # 6: Minutes (?:(\d+(?:[.,]\d+)?)S)? # 7: Seconds )? | (\d+)W # 8: Weeks ) $)x.freeze UUID_PREFIX = '_' # Checks if the x509 cert provided is expired # # @param cert [Certificate] The x509 certificate # def self.is_cert_expired(cert) if cert.is_a?(String) cert = OpenSSL::X509::Certificate.new(cert) end return cert.not_after < Time.now end # Interprets a ISO8601 duration value relative to a given timestamp. # # @param duration [String] The duration, as a string. # @param timestamp [Integer] The unix timestamp we should apply the # duration to. Optional, default to the # current time. # # @return [Integer] The new timestamp, after the duration is applied. # def self.parse_duration(duration, timestamp=Time.now.utc) return nil if RUBY_VERSION < '1.9' # 1.8.7 not supported matches = duration.match(DURATION_FORMAT) if matches.nil? raise Exception.new("Invalid ISO 8601 duration") end sign = matches[1] == '-' ? -1 : 1 durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks = matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 } initial_datetime = Time.at(timestamp).utc.to_datetime final_datetime = initial_datetime.next_year(durYears) final_datetime = final_datetime.next_month(durMonths) final_datetime = final_datetime.next_day((7*durWeeks) + durDays) final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds return final_timestamp end # Return a properly formatted x509 certificate # # @param cert [String] The original certificate # @return [String] The formatted certificate # def self.format_cert(cert) # don't try to format an encoded certificate or if is empty or nil if cert.respond_to?(:ascii_only?) return cert if cert.nil? || cert.empty? || !cert.ascii_only? else return cert if cert.nil? || cert.empty? || cert.match(/\x0d/) end if cert.scan(/BEGIN CERTIFICATE/).length > 1 formatted_cert = [] cert.scan(/-{5}BEGIN CERTIFICATE-{5}[\n\r]?.*?-{5}END CERTIFICATE-{5}[\n\r]?/m) {|c| formatted_cert << format_cert(c) } formatted_cert.join("\n") else cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "") cert = cert.gsub(/\r/, "") cert = cert.gsub(/\n/, "") cert = cert.gsub(/\s/, "") cert = cert.scan(/.{1,64}/) cert = cert.join("\n") "-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----" end end # Return a properly formatted private key # # @param key [String] The original private key # @return [String] The formatted private key # def self.format_private_key(key) # don't try to format an encoded private key or if is empty return key if key.nil? || key.empty? || key.match(/\x0d/) # is this an rsa key? rsa_key = key.match("RSA PRIVATE KEY") key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "") key = key.gsub(/\n/, "") key = key.gsub(/\r/, "") key = key.gsub(/\s/, "") key = key.scan(/.{1,64}/) key = key.join("\n") key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY" "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----" end # Build the Query String signature that will be used in the HTTP-Redirect binding # to generate the Signature # @param params [Hash] Parameters to build the Query String # @option params [String] :type 'SAMLRequest' or 'SAMLResponse' # @option params [String] :data Base64 encoded SAMLRequest or SAMLResponse # @option params [String] :relay_state The RelayState parameter # @option params [String] :sig_alg The SigAlg parameter # @return [String] The Query String # def self.build_query(params) type, data, relay_state, sig_alg = [:type, :data, :relay_state, :sig_alg].map { |k| params[k]} url_string = "#{type}=#{CGI.escape(data)}" url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state url_string << "&SigAlg=#{CGI.escape(sig_alg)}" end # Reconstruct a canonical query string from raw URI-encoded parts, to be used in verifying a signature # # @param params [Hash] Parameters to build the Query String # @option params [String] :type 'SAMLRequest' or 'SAMLResponse' # @option params [String] :raw_data URI-encoded, base64 encoded SAMLRequest or SAMLResponse, as sent by IDP # @option params [String] :raw_relay_state URI-encoded RelayState parameter, as sent by IDP # @option params [String] :raw_sig_alg URI-encoded SigAlg parameter, as sent by IDP # @return [String] The Query String # def self.build_query_from_raw_parts(params) type, raw_data, raw_relay_state, raw_sig_alg = [:type, :raw_data, :raw_relay_state, :raw_sig_alg].map { |k| params[k]} url_string = "#{type}=#{raw_data}" url_string << "&RelayState=#{raw_relay_state}" if raw_relay_state url_string << "&SigAlg=#{raw_sig_alg}" end # Prepare raw GET parameters (build them from normal parameters # if not provided). # # @param rawparams [Hash] Raw GET Parameters # @param params [Hash] GET Parameters # @param lowercase_url_encoding [bool] Lowercase URL Encoding (For ADFS urlencode compatiblity) # @return [Hash] New raw parameters # def self.prepare_raw_get_params(rawparams, params, lowercase_url_encoding=false) rawparams ||= {} if rawparams['SAMLRequest'].nil? && !params['SAMLRequest'].nil? rawparams['SAMLRequest'] = escape_request_param(params['SAMLRequest'], lowercase_url_encoding) end if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil? rawparams['SAMLResponse'] = escape_request_param(params['SAMLResponse'], lowercase_url_encoding) end if rawparams['RelayState'].nil? && !params['RelayState'].nil? rawparams['RelayState'] = escape_request_param(params['RelayState'], lowercase_url_encoding) end if rawparams['SigAlg'].nil? && !params['SigAlg'].nil? rawparams['SigAlg'] = escape_request_param(params['SigAlg'], lowercase_url_encoding) end rawparams end def self.escape_request_param(param, lowercase_url_encoding) CGI.escape(param).tap do |escaped| next unless lowercase_url_encoding escaped.gsub!(/%[A-Fa-f0-9]{2}/) { |match| match.downcase } end end # Validate the Signature parameter sent on the HTTP-Redirect binding # @param params [Hash] Parameters to be used in the validation process # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate # @option params [String] sig_alg The SigAlg parameter # @option params [String] signature The Signature parameter (base64 encoded) # @option params [String] query_string The full GET Query String to be compared # @return [Boolean] True if the Signature is valid, False otherwise # def self.verify_signature(params) cert, sig_alg, signature, query_string = [:cert, :sig_alg, :signature, :query_string].map { |k| params[k]} signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(sig_alg) return cert.public_key.verify(signature_algorithm.new, Base64.decode64(signature), query_string) end # Build the status error message # @param status_code [String] StatusCode value # @param status_message [Strig] StatusMessage value # @return [String] The status error message def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil) unless raw_status_code.nil? if raw_status_code.include? "|" status_codes = raw_status_code.split(' | ') values = status_codes.collect do |status_code| status_code.split(':').last end printable_code = values.join(" => ") else printable_code = raw_status_code.split(':').last end error_msg << ', was ' + printable_code end unless status_message.nil? error_msg << ' -> ' + status_message end error_msg end # Obtains the decrypted string from an Encrypted node element in XML # @param encrypted_node [REXML::Element] The Encrypted element # @param private_key [OpenSSL::PKey::RSA] The Service provider private key # @return [String] The decrypted data def self.decrypt_data(encrypted_node, private_key) encrypt_data = REXML::XPath.first( encrypted_node, "./xenc:EncryptedData", { 'xenc' => XENC } ) symmetric_key = retrieve_symmetric_key(encrypt_data, private_key) cipher_value = REXML::XPath.first( encrypt_data, "./xenc:CipherData/xenc:CipherValue", { 'xenc' => XENC } ) node = Base64.decode64(element_text(cipher_value)) encrypt_method = REXML::XPath.first( encrypt_data, "./xenc:EncryptionMethod", { 'xenc' => XENC } ) algorithm = encrypt_method.attributes['Algorithm'] retrieve_plaintext(node, symmetric_key, algorithm) end # Obtains the symmetric key from the EncryptedData element # @param encrypt_data [REXML::Element] The EncryptedData element # @param private_key [OpenSSL::PKey::RSA] The Service provider private key # @return [String] The symmetric key def self.retrieve_symmetric_key(encrypt_data, private_key) encrypted_key = REXML::XPath.first( encrypt_data, "./ds:KeyInfo/xenc:EncryptedKey | ./KeyInfo/xenc:EncryptedKey | //xenc:EncryptedKey[@Id=$id]", { "ds" => DSIG, "xenc" => XENC }, { "id" => self.retrieve_symetric_key_reference(encrypt_data) } ) encrypted_symmetric_key_element = REXML::XPath.first( encrypted_key, "./xenc:CipherData/xenc:CipherValue", "xenc" => XENC ) cipher_text = Base64.decode64(element_text(encrypted_symmetric_key_element)) encrypt_method = REXML::XPath.first( encrypted_key, "./xenc:EncryptionMethod", "xenc" => XENC ) algorithm = encrypt_method.attributes['Algorithm'] retrieve_plaintext(cipher_text, private_key, algorithm) end def self.retrieve_symetric_key_reference(encrypt_data) REXML::XPath.first( encrypt_data, "substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')", { "ds" => DSIG } ) end # Obtains the deciphered text # @param cipher_text [String] The ciphered text # @param symmetric_key [String] The symetric key used to encrypt the text # @param algorithm [String] The encrypted algorithm # @return [String] The deciphered text def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm) case algorithm when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(128, :GCM).decrypt when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(192, :GCM).decrypt when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key end if cipher iv_len = cipher.iv_len data = cipher_text[iv_len..-1] cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1] assertion_plaintext = cipher.update(data) assertion_plaintext << cipher.final elsif auth_cipher iv_len, text_len, tag_len = auth_cipher.iv_len, cipher_text.length, 16 data = cipher_text[iv_len..text_len-1-tag_len] auth_cipher.padding = 0 auth_cipher.key = symmetric_key auth_cipher.iv = cipher_text[0..iv_len-1] auth_cipher.auth_data = '' auth_cipher.auth_tag = cipher_text[text_len-tag_len..-1] assertion_plaintext = auth_cipher.update(data) assertion_plaintext << auth_cipher.final elsif rsa rsa.private_decrypt(cipher_text) elsif oaep oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) else cipher_text end end def self.set_prefix(value) UUID_PREFIX.replace value end def self.uuid "#{UUID_PREFIX}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}") end # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed, # then the fully-qualified domain name and the host should performa a case-insensitive match, per the # RFC for URIs. If Rails can not parse the string in to URL pieces, return a boolean match of the # two strings. This maintains the previous functionality. # @return [Boolean] def self.uri_match?(destination_url, settings_url) dest_uri = URI.parse(destination_url) acs_uri = URI.parse(settings_url) if dest_uri.scheme.nil? || acs_uri.scheme.nil? || dest_uri.host.nil? || acs_uri.host.nil? raise URI::InvalidURIError else dest_uri.scheme.downcase == acs_uri.scheme.downcase && dest_uri.host.downcase == acs_uri.host.downcase && dest_uri.path == acs_uri.path && dest_uri.query == acs_uri.query end rescue URI::InvalidURIError original_uri_match?(destination_url, settings_url) end # If Rails' URI.parse can't match to valid URL, default back to the original matching service. # @return [Boolean] def self.original_uri_match?(destination_url, settings_url) destination_url == settings_url end # Given a REXML::Element instance, return the concatenation of all child text nodes. Assumes # that there all children other than text nodes can be ignored (e.g. comments). If nil is # passed, nil will be returned. def self.element_text(element) element.texts.map(&:value).join if element end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/error_handling.rb0000644000004100000410000000127514405647360023374 0ustar www-datawww-datarequire "onelogin/ruby-saml/validation_error" module OneLogin module RubySaml module ErrorHandling attr_accessor :errors # Append the cause to the errors array, and based on the value of soft, return false or raise # an exception. soft_override is provided as a means of overriding the object's notion of # soft for just this invocation. def append_error(error_msg, soft_override = nil) @errors << error_msg unless soft_override.nil? ? soft : soft_override raise ValidationError.new(error_msg) end false end # Reset the errors array def reset_errors! @errors = [] end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/logoutresponse.rb0000644000004100000410000002432714405647360023472 0ustar www-datawww-datarequire "xml_security" require "onelogin/ruby-saml/saml_message" require "time" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Logout Response (SLO IdP initiated, Parser) # class Logoutresponse < SamlMessage include ErrorHandling # OneLogin::RubySaml::Settings Toolkit settings attr_accessor :settings attr_reader :document attr_reader :response attr_reader :options attr_accessor :soft # Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class. # @param response [String] A UUEncoded logout response from the IdP. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param options [Hash] Extra parameters. # :matches_request_id It will validate that the logout response matches the ID of the request. # :get_params GET Parameters, including the SAMLResponse # :relax_signature_validation to accept signatures if no idp certificate registered on settings # # @raise [ArgumentError] if response is nil # def initialize(response, settings = nil, options = {}) @errors = [] raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? @settings = settings if settings.nil? || settings.soft.nil? @soft = true else @soft = settings.soft end @options = options @response = decode_raw_saml(response, settings) @document = XMLSecurity::SignedDocument.new(@response) end def response_id id(document) end # Checks if the Status has the "Success" code # @return [Boolean] True if the StatusCode is Sucess # @raise [ValidationError] if soft == false and validation fails # def success? return status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" end # @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists. # def in_response_to @in_response_to ||= begin node = REXML::XPath.first( document, "/p:LogoutResponse", { "p" => PROTOCOL } ) node.nil? ? nil : node.attributes['InResponseTo'] end end # @return [String] Gets the Issuer from the Logout Response. # def issuer @issuer ||= begin node = REXML::XPath.first( document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION } ) Utils.element_text(node) end end # @return [String] Gets the StatusCode from a Logout Response. # def status_code @status_code ||= begin node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL }) node.nil? ? nil : node.attributes["Value"] end end def status_message @status_message ||= begin node = REXML::XPath.first( document, "/p:LogoutResponse/p:Status/p:StatusMessage", { "p" => PROTOCOL } ) Utils.element_text(node) end end # Aux function to validate the Logout Response # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) # @return [Boolean] TRUE if the SAML Response is valid # @raise [ValidationError] if soft == false and validation fails # def validate(collect_errors = false) reset_errors! validations = [ :valid_state?, :validate_success_status, :validate_structure, :valid_in_response_to?, :valid_issuer?, :validate_signature ] if collect_errors validations.each { |validation| send(validation) } @errors.empty? else validations.all? { |validation| send(validation) } end end private # Validates the Status of the Logout Response # If fails, the error is added to the errors array, including the StatusCode returned and the Status Message. # @return [Boolean] True if the Logout Response contains a Success code, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_success_status return true if success? error_msg = 'The status code of the Logout Response was not Success' status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) append_error(status_error_msg) end # Validates the Logout Response against the specified schema. # @return [Boolean] True if the XML is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_structure unless valid_saml?(document, soft) return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd") end true end # Validates that the Logout Response provided in the initialization is not empty, # also check that the setting and the IdP cert were also provided # @return [Boolean] True if the required info is found, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def valid_state? return append_error("Blank logout response") if response.empty? return append_error("No settings on logout response") if settings.nil? return append_error("No sp_entity_id in settings of the logout response") if settings.sp_entity_id.nil? if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil? return append_error("No fingerprint or certificate on settings of the logout response") end true end # Validates if a provided :matches_request_id matchs the inResponseTo value. # @param soft [String|nil] request_id The ID of the Logout Request sent by this SP to the IdP (if was sent any) # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def valid_in_response_to? return true unless options.has_key? :matches_request_id return true if options[:matches_request_id].nil? return true unless options[:matches_request_id] != in_response_to error_msg = "The InResponseTo of the Logout Response: #{in_response_to}, does not match the ID of the Logout Request sent by the SP: #{options[:matches_request_id]}" append_error(error_msg) end # Validates the Issuer of the Logout Response # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def valid_issuer? return true if settings.idp_entity_id.nil? || issuer.nil? unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") end true end # Validates the Signature if it exists and the GET parameters are provided # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_signature return true unless !options.nil? return true unless options.has_key? :get_params return true unless options[:get_params].has_key? 'Signature' options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding]) if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil? options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg']) end idp_cert = settings.get_idp_cert idp_certs = settings.get_idp_cert_multi if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?) return options.has_key? :relax_signature_validation end query_string = OneLogin::RubySaml::Utils.build_query_from_raw_parts( :type => 'SAMLResponse', :raw_data => options[:raw_get_params]['SAMLResponse'], :raw_relay_state => options[:raw_get_params]['RelayState'], :raw_sig_alg => options[:raw_get_params]['SigAlg'] ) expired = false if idp_certs.nil? || idp_certs[:signing].empty? valid = OneLogin::RubySaml::Utils.verify_signature( :cert => idp_cert, :sig_alg => options[:get_params]['SigAlg'], :signature => options[:get_params]['Signature'], :query_string => query_string ) if valid && settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) expired = true end end else valid = false idp_certs[:signing].each do |signing_idp_cert| valid = OneLogin::RubySaml::Utils.verify_signature( :cert => signing_idp_cert, :sig_alg => options[:get_params]['SigAlg'], :signature => options[:get_params]['Signature'], :query_string => query_string ) if valid if settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert) expired = true end end break end end end if expired error_msg = "IdP x509 certificate expired" return append_error(error_msg) end unless valid error_msg = "Invalid Signature on Logout Response" return append_error(error_msg) end true end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/authrequest.rb0000644000004100000410000001740314405647360022751 0ustar www-datawww-datarequire "rexml/document" require "onelogin/ruby-saml/logging" require "onelogin/ruby-saml/saml_message" require "onelogin/ruby-saml/utils" require "onelogin/ruby-saml/setting_error" # Only supports SAML 2.0 module OneLogin module RubySaml include REXML # SAML2 Authentication. AuthNRequest (SSO SP initiated, Builder) # class Authrequest < SamlMessage # AuthNRequest ID attr_accessor :uuid # Initializes the AuthNRequest. An Authrequest Object that is an extension of the SamlMessage class. # Asigns an ID, a random uuid. # def initialize @uuid = OneLogin::RubySaml::Utils.uuid end def request_id @uuid end # Creates the AuthNRequest string. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState # @return [String] AuthNRequest string that includes the SAMLRequest # def create(settings, params = {}) params = create_params(settings, params) params_prefix = (settings.idp_sso_service_url =~ /\?/) ? '&' : '?' saml_request = CGI.escape(params.delete("SAMLRequest")) request_params = "#{params_prefix}SAMLRequest=#{saml_request}" params.each_pair do |key, value| request_params << "&#{key}=#{CGI.escape(value.to_s)}" end raise SettingError.new "Invalid settings, idp_sso_service_url is not set!" if settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty? @login_url = settings.idp_sso_service_url + request_params end # Creates the Get parameters for the request. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState # @return [Hash] Parameters # def create_params(settings, params={}) # The method expects :RelayState but sometimes we get 'RelayState' instead. # Based on the HashWithIndifferentAccess value in Rails we could experience # conflicts so this line will solve them. relay_state = params[:RelayState] || params['RelayState'] if relay_state.nil? params.delete(:RelayState) params.delete('RelayState') end request_doc = create_authentication_xml_doc(settings) request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values request = "" request_doc.write(request) Logging.debug "Created AuthnRequest: #{request}" request = deflate(request) if settings.compress_request base64_request = encode(request) request_params = {"SAMLRequest" => base64_request} if settings.idp_sso_service_binding == Utils::BINDINGS[:redirect] && settings.security[:authn_requests_signed] && settings.private_key params['SigAlg'] = settings.security[:signature_method] url_string = OneLogin::RubySaml::Utils.build_query( :type => 'SAMLRequest', :data => base64_request, :relay_state => relay_state, :sig_alg => params['SigAlg'] ) sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]) signature = settings.get_sp_key.sign(sign_algorithm.new, url_string) params['Signature'] = encode(signature) end params.each_pair do |key, value| request_params[key] = value.to_s end request_params end # Creates the SAMLRequest String. # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @return [String] The SAMLRequest String. # def create_authentication_xml_doc(settings) document = create_xml_document(settings) sign_document(document, settings) end def create_xml_document(settings) time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") request_doc = XMLSecurity::Document.new request_doc.uuid = uuid root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" } root.attributes['ID'] = uuid root.attributes['IssueInstant'] = time root.attributes['Version'] = "2.0" root.attributes['Destination'] = settings.idp_sso_service_url unless settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty? root.attributes['IsPassive'] = settings.passive unless settings.passive.nil? root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil? root.attributes["AttributeConsumingServiceIndex"] = settings.attributes_index unless settings.attributes_index.nil? root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil? # Conditionally defined elements based on settings if settings.assertion_consumer_service_url != nil root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url end if settings.sp_entity_id != nil issuer = root.add_element "saml:Issuer" issuer.text = settings.sp_entity_id end if settings.name_identifier_value_requested != nil subject = root.add_element "saml:Subject" nameid = subject.add_element "saml:NameID" nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format nameid.text = settings.name_identifier_value_requested subject_confirmation = subject.add_element "saml:SubjectConfirmation" subject_confirmation.attributes['Method'] = "urn:oasis:names:tc:SAML:2.0:cm:bearer" end if settings.name_identifier_format != nil root.add_element "samlp:NameIDPolicy", { # Might want to make AllowCreate a setting? "AllowCreate" => "true", "Format" => settings.name_identifier_format } end if settings.authn_context || settings.authn_context_decl_ref if settings.authn_context_comparison != nil comparison = settings.authn_context_comparison else comparison = 'exact' end requested_context = root.add_element "samlp:RequestedAuthnContext", { "Comparison" => comparison, } if settings.authn_context != nil authn_contexts_class_ref = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context] authn_contexts_class_ref.each do |authn_context_class_ref| class_ref = requested_context.add_element "saml:AuthnContextClassRef" class_ref.text = authn_context_class_ref end end if settings.authn_context_decl_ref != nil authn_contexts_decl_refs = settings.authn_context_decl_ref.is_a?(Array) ? settings.authn_context_decl_ref : [settings.authn_context_decl_ref] authn_contexts_decl_refs.each do |authn_context_decl_ref| decl_ref = requested_context.add_element "saml:AuthnContextDeclRef" decl_ref.text = authn_context_decl_ref end end end request_doc end def sign_document(document, settings) if settings.idp_sso_service_binding == Utils::BINDINGS[:post] && settings.security[:authn_requests_signed] && settings.private_key && settings.certificate private_key = settings.get_sp_key cert = settings.get_sp_cert document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method]) end document end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/response.rb0000644000004100000410000012076414405647360022242 0ustar www-datawww-datarequire "xml_security" require "onelogin/ruby-saml/attributes" require "time" require "nokogiri" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Authentication Response. SAML Response # class Response < SamlMessage include ErrorHandling ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" DSIG = "http://www.w3.org/2000/09/xmldsig#" XENC = "http://www.w3.org/2001/04/xmlenc#" # TODO: Settings should probably be initialized too... WDYT? # OneLogin::RubySaml::Settings Toolkit settings attr_accessor :settings attr_reader :document attr_reader :decrypted_document attr_reader :response attr_reader :options attr_accessor :soft # Response available options # This is not a whitelist to allow people extending OneLogin::RubySaml:Response # and pass custom options AVAILABLE_OPTIONS = [ :allowed_clock_drift, :check_duplicated_attributes, :matches_request_id, :settings, :skip_audience, :skip_authnstatement, :skip_conditions, :skip_destination, :skip_recipient_check, :skip_subject_confirmation ] # TODO: Update the comment on initialize to describe every option # Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class. # @param response [String] A UUEncoded SAML response from the IdP. # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object # Or some options for the response validation process like skip the conditions validation # with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift # or :matches_request_id that will validate that the response matches the ID of the request, # or skip the subject confirmation validation with the :skip_subject_confirmation option # or skip the recipient validation of the subject confirmation element with :skip_recipient_check option # or skip the audience validation with :skip_audience option # def initialize(response, options = {}) raise ArgumentError.new("Response cannot be nil") if response.nil? @errors = [] @options = options @soft = true unless options[:settings].nil? @settings = options[:settings] unless @settings.soft.nil? @soft = @settings.soft end end @response = decode_raw_saml(response, settings) @document = XMLSecurity::SignedDocument.new(@response, @errors) if assertion_encrypted? @decrypted_document = generate_decrypted_document end end # Validates the SAML Response with the default values (soft = true) # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) # @return [Boolean] TRUE if the SAML Response is valid # def is_valid?(collect_errors = false) validate(collect_errors) end # @return [String] the NameID provided by the SAML response from the IdP. # def name_id @name_id ||= Utils.element_text(name_id_node) end alias_method :nameid, :name_id # @return [String] the NameID Format provided by the SAML response from the IdP. # def name_id_format @name_id_format ||= if name_id_node && name_id_node.attribute("Format") name_id_node.attribute("Format").value end end alias_method :nameid_format, :name_id_format # @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP. # def name_id_spnamequalifier @name_id_spnamequalifier ||= if name_id_node && name_id_node.attribute("SPNameQualifier") name_id_node.attribute("SPNameQualifier").value end end # @return [String] the NameID NameQualifier provided by the SAML response from the IdP. # def name_id_namequalifier @name_id_namequalifier ||= if name_id_node && name_id_node.attribute("NameQualifier") name_id_node.attribute("NameQualifier").value end end # Gets the SessionIndex from the AuthnStatement. # Could be used to be stored in the local session in order # to be used in a future Logout Request that the SP could # send to the IdP, to set what specific session must be deleted # @return [String] SessionIndex Value # def sessionindex @sessionindex ||= begin node = xpath_first_from_signed_assertion('/a:AuthnStatement') node.nil? ? nil : node.attributes['SessionIndex'] end end # Gets the Attributes from the AttributeStatement element. # # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+ # For backwards compatibility ruby-saml returns by default only the first value for a given attribute with # attributes['name'] # To get all of the attributes, use: # attributes.multi('name') # Or turn off the compatibility: # OneLogin::RubySaml::Attributes.single_value_compatibility = false # Now this will return an array: # attributes['name'] # # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection. # @raise [ValidationError] if there are 2+ Attribute with the same Name # def attributes @attr_statements ||= begin attributes = Attributes.new stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement') stmt_elements.each do |stmt_element| stmt_element.elements.each do |attr_element| if attr_element.name == "EncryptedAttribute" node = decrypt_attribute(attr_element.dup) else node = attr_element end name = node.attributes["Name"] if options[:check_duplicated_attributes] && attributes.include?(name) raise ValidationError.new("Found an Attribute element with duplicated Name") end values = node.elements.collect{|e| if (e.elements.nil? || e.elements.size == 0) # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1" # otherwise the value is to be regarded as empty. ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : Utils.element_text(e) # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to # identify the subject in an SP rather than email or other less opaque attributes # NameQualifier, if present is prefixed with a "/" to the value else REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n| base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : '' "#{base_path}#{Utils.element_text(n)}" end end } attributes.add(name, values.flatten) end end attributes end end # Gets the SessionNotOnOrAfter from the AuthnStatement. # Could be used to set the local session expiration (expire at latest) # @return [String] The SessionNotOnOrAfter value # def session_expires_at @expires_at ||= begin node = xpath_first_from_signed_assertion('/a:AuthnStatement') node.nil? ? nil : parse_time(node, "SessionNotOnOrAfter") end end # Checks if the Status has the "Success" code # @return [Boolean] True if the StatusCode is Sucess # def success? status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" end # @return [String] StatusCode value from a SAML Response. # def status_code @status_code ||= begin nodes = REXML::XPath.match( document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL } ) if nodes.size == 1 node = nodes[0] code = node.attributes["Value"] if node && node.attributes unless code == "urn:oasis:names:tc:SAML:2.0:status:Success" nodes = REXML::XPath.match( document, "/p:Response/p:Status/p:StatusCode/p:StatusCode", { "p" => PROTOCOL } ) statuses = nodes.collect do |inner_node| inner_node.attributes["Value"] end code = [code, statuses].flatten.join(" | ") end code end end end # @return [String] the StatusMessage value from a SAML Response. # def status_message @status_message ||= begin nodes = REXML::XPath.match( document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL } ) if nodes.size == 1 Utils.element_text(nodes.first) end end end # Gets the Condition Element of the SAML Response if exists. # (returns the first node that matches the supplied xpath) # @return [REXML::Element] Conditions Element if exists # def conditions @conditions ||= xpath_first_from_signed_assertion('/a:Conditions') end # Gets the NotBefore Condition Element value. # @return [Time] The NotBefore value in Time format # def not_before @not_before ||= parse_time(conditions, "NotBefore") end # Gets the NotOnOrAfter Condition Element value. # @return [Time] The NotOnOrAfter value in Time format # def not_on_or_after @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter") end # Gets the Issuers (from Response and Assertion). # (returns the first node that matches the supplied xpath from the Response and from the Assertion) # @return [Array] Array with the Issuers (REXML::Element) # def issuers @issuers ||= begin issuer_response_nodes = REXML::XPath.match( document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION } ) unless issuer_response_nodes.size == 1 error_msg = "Issuer of the Response not found or multiple." raise ValidationError.new(error_msg) end issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer") unless issuer_assertion_nodes.size == 1 error_msg = "Issuer of the Assertion not found or multiple." raise ValidationError.new(error_msg) end nodes = issuer_response_nodes + issuer_assertion_nodes nodes.map { |node| Utils.element_text(node) }.compact.uniq end end # @return [String|nil] The InResponseTo attribute from the SAML Response. # def in_response_to @in_response_to ||= begin node = REXML::XPath.first( document, "/p:Response", { "p" => PROTOCOL } ) node.nil? ? nil : node.attributes['InResponseTo'] end end # @return [String|nil] Destination attribute from the SAML Response. # def destination @destination ||= begin node = REXML::XPath.first( document, "/p:Response", { "p" => PROTOCOL } ) node.nil? ? nil : node.attributes['Destination'] end end # @return [Array] The Audience elements from the Contitions of the SAML Response. # def audiences @audiences ||= begin nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience') nodes.map { |node| Utils.element_text(node) }.reject(&:empty?) end end # returns the allowed clock drift on timing validation # @return [Float] def allowed_clock_drift options[:allowed_clock_drift].to_f.abs + Float::EPSILON end # Checks if the SAML Response contains or not an EncryptedAssertion element # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element # def assertion_encrypted? ! REXML::XPath.first( document, "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", { "p" => PROTOCOL, "a" => ASSERTION } ).nil? end def response_id id(document) end def assertion_id @assertion_id ||= begin node = xpath_first_from_signed_assertion("") node.nil? ? nil : node.attributes['ID'] end end private # Validates the SAML Response (calls several validation methods) # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true) # @return [Boolean] True if the SAML Response is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate(collect_errors = false) reset_errors! return false unless validate_response_state validations = [ :validate_version, :validate_id, :validate_success_status, :validate_num_assertion, :validate_no_duplicated_attributes, :validate_signed_elements, :validate_structure, :validate_in_response_to, :validate_one_conditions, :validate_conditions, :validate_one_authnstatement, :validate_audience, :validate_destination, :validate_issuer, :validate_session_expiration, :validate_subject_confirmation, :validate_name_id, :validate_signature ] if collect_errors validations.each { |validation| send(validation) } @errors.empty? else validations.all? { |validation| send(validation) } end end # Validates the Status of the SAML Response # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false # @raise [ValidationError] if soft == false and validation fails # def validate_success_status return true if success? error_msg = 'The status code of the Response was not Success' status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message) append_error(status_error_msg) end # Validates the SAML Response against the specified schema. # @return [Boolean] True if the XML is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_structure structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd" unless valid_saml?(document, soft) return append_error(structure_error_msg) end unless decrypted_document.nil? unless valid_saml?(decrypted_document, soft) return append_error(structure_error_msg) end end true end # Validates that the SAML Response provided in the initialization is not empty, # also check that the setting and the IdP cert were also provided # @return [Boolean] True if the required info is found, false otherwise # def validate_response_state return append_error("Blank response") if response.nil? || response.empty? return append_error("No settings on response") if settings.nil? if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil? return append_error("No fingerprint or certificate on settings") end true end # Validates that the SAML Response contains an ID # If fails, the error is added to the errors array. # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False # def validate_id unless response_id return append_error("Missing ID attribute on SAML Response") end true end # Validates the SAML version (2.0) # If fails, the error is added to the errors array. # @return [Boolean] True if the SAML Response is 2.0, otherwise returns False # def validate_version unless version(document) == "2.0" return append_error("Unsupported SAML version") end true end # Validates that the SAML Response only contains a single Assertion (encrypted or not). # If fails, the error is added to the errors array. # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False # def validate_num_assertion error_msg = "SAML Response must contain 1 assertion" assertions = REXML::XPath.match( document, "//a:Assertion", { "a" => ASSERTION } ) encrypted_assertions = REXML::XPath.match( document, "//a:EncryptedAssertion", { "a" => ASSERTION } ) unless assertions.size + encrypted_assertions.size == 1 return append_error(error_msg) end unless decrypted_document.nil? assertions = REXML::XPath.match( decrypted_document, "//a:Assertion", { "a" => ASSERTION } ) unless assertions.size == 1 return append_error(error_msg) end end true end # Validates that there are not duplicated attributes # If fails, the error is added to the errors array # @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_no_duplicated_attributes if options[:check_duplicated_attributes] begin attributes rescue ValidationError => e return append_error(e.message) end end true end # Validates the Signed elements # If fails, the error is added to the errors array # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response # an are a Response or an Assertion Element, otherwise False if soft=True # def validate_signed_elements signature_nodes = REXML::XPath.match( decrypted_document.nil? ? document : decrypted_document, "//ds:Signature", {"ds"=>DSIG} ) signed_elements = [] verified_seis = [] verified_ids = [] signature_nodes.each do |signature_node| signed_element = signature_node.parent.name if signed_element != 'Response' && signed_element != 'Assertion' return append_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected") end if signature_node.parent.attributes['ID'].nil? return append_error("Signed Element must contain an ID. SAML Response rejected") end id = signature_node.parent.attributes.get_attribute("ID").value if verified_ids.include?(id) return append_error("Duplicated ID. SAML Response rejected") end verified_ids.push(id) # Check that reference URI matches the parent ID and no duplicate References or IDs ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG}) if ref uri = ref.attributes.get_attribute("URI") if uri && !uri.value.empty? sei = uri.value[1..-1] unless sei == id return append_error("Found an invalid Signed Element. SAML Response rejected") end if verified_seis.include?(sei) return append_error("Duplicated Reference URI. SAML Response rejected") end verified_seis.push(sei) end end signed_elements << signed_element end unless signature_nodes.length < 3 && !signed_elements.empty? return append_error("Found an unexpected number of Signature Element. SAML Response rejected") end if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion") return append_error("The Assertion of the Response is not signed and the SP requires it") end true end # Validates if the provided request_id match the inResponseTo value. # If fails, the error is added to the errors array # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_in_response_to return true unless options.has_key? :matches_request_id return true if options[:matches_request_id].nil? return true unless options[:matches_request_id] != in_response_to error_msg = "The InResponseTo of the Response: #{in_response_to}, does not match the ID of the AuthNRequest sent by the SP: #{options[:matches_request_id]}" append_error(error_msg) end # Validates the Audience, (If the Audience match the Service Provider EntityID) # If the response was initialized with the :skip_audience option, this validation is skipped, # If fails, the error is added to the errors array # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_audience return true if options[:skip_audience] return true if settings.sp_entity_id.nil? || settings.sp_entity_id.empty? if audiences.empty? return true unless settings.security[:strict_audience_validation] return append_error("Invalid Audiences. The element contained only empty elements. Expected audience #{settings.sp_entity_id}.") end unless audiences.include? settings.sp_entity_id s = audiences.count > 1 ? 's' : ''; error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}" return append_error(error_msg) end true end # Validates the Destination, (If the SAML Response is received where expected). # If the response was initialized with the :skip_destination option, this validation is skipped, # If fails, the error is added to the errors array # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False # def validate_destination return true if destination.nil? return true if options[:skip_destination] if destination.empty? error_msg = "The response has an empty Destination value" return append_error(error_msg) end return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty? unless OneLogin::RubySaml::Utils.uri_match?(destination, settings.assertion_consumer_service_url) error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}" return append_error(error_msg) end true end # Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. # (If the response was initialized with the :skip_conditions option, this validation is skipped) # If fails, the error is added to the errors array # @return [Boolean] True if there is a conditions element and is unique # def validate_one_conditions return true if options[:skip_conditions] conditions_nodes = xpath_from_signed_assertion('/a:Conditions') unless conditions_nodes.size == 1 error_msg = "The Assertion must include one Conditions element" return append_error(error_msg) end true end # Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. # If fails, the error is added to the errors array # @return [Boolean] True if there is a authnstatement element and is unique # def validate_one_authnstatement return true if options[:skip_authnstatement] authnstatement_nodes = xpath_from_signed_assertion('/a:AuthnStatement') unless authnstatement_nodes.size == 1 error_msg = "The Assertion must include one AuthnStatement element" return append_error(error_msg) end true end # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped, # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value) # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_conditions return true if conditions.nil? return true if options[:skip_conditions] now = Time.now.utc if not_before && now < (not_before - allowed_clock_drift) error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})" return append_error(error_msg) end if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift) error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})" return append_error(error_msg) end true end # Validates the Issuer (Of the SAML Response and the SAML Assertion) # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not) # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_issuer return true if settings.idp_entity_id.nil? begin obtained_issuers = issuers rescue ValidationError => e return append_error(e.message) end obtained_issuers.each do |issuer| unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id) error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>" return append_error(error_msg) end end true end # Validates that the Session haven't expired (If the response was initialized with the :allowed_clock_drift option, # this time validation is relaxed by the allowed_clock_drift value) # If fails, the error is added to the errors array # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not) # @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_session_expiration return true if session_expires_at.nil? now = Time.now.utc unless now < (session_expires_at + allowed_clock_drift) error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response" return append_error(error_msg) end true end # Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option, # timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the # :skip_subject_confirmation option, this validation is skipped) # There is also an optional Recipient check # If fails, the error is added to the errors array # @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_subject_confirmation return true if options[:skip_subject_confirmation] valid_subject_confirmation = false subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation') now = Time.now.utc subject_confirmation_nodes.each do |subject_confirmation| if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer' next end confirmation_data_node = REXML::XPath.first( subject_confirmation, 'a:SubjectConfirmationData', { "a" => ASSERTION } ) next unless confirmation_data_node attrs = confirmation_data_node.attributes next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) || (attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) || (attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) || (attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url) valid_subject_confirmation = true break end if !valid_subject_confirmation error_msg = "A valid SubjectConfirmation was not found on this Response" return append_error(error_msg) end true end # Validates the NameID element def validate_name_id if name_id_node.nil? if settings.security[:want_name_id] return append_error("No NameID element found in the assertion of the Response") end else if name_id.nil? || name_id.empty? return append_error("An empty NameID value found") end unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty? if name_id_spnamequalifier != settings.sp_entity_id return append_error("The SPNameQualifier value mistmatch the SP entityID value.") end end end true end # Validates the Signature # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True # @raise [ValidationError] if soft == false and validation fails # def validate_signature error_msg = "Invalid Signature on SAML Response" # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response # otherwise, review if the decrypted assertion contains a signature sig_elements = REXML::XPath.match( document, "/p:Response[@ID=$id]/ds:Signature", { "p" => PROTOCOL, "ds" => DSIG }, { 'id' => document.signed_element_id } ) use_original = sig_elements.size == 1 || decrypted_document.nil? doc = use_original ? document : decrypted_document # Check signature nodes if sig_elements.nil? || sig_elements.size == 0 sig_elements = REXML::XPath.match( doc, "/p:Response/a:Assertion[@ID=$id]/ds:Signature", {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG}, { 'id' => doc.signed_element_id } ) end if sig_elements.size != 1 if sig_elements.size == 0 append_error("Signed element id ##{doc.signed_element_id} is not found") else append_error("Signed element id ##{doc.signed_element_id} is found more than once") end return append_error(error_msg) end old_errors = @errors.clone idp_certs = settings.get_idp_cert_multi if idp_certs.nil? || idp_certs[:signing].empty? opts = {} opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm idp_cert = settings.get_idp_cert fingerprint = settings.get_fingerprint opts[:cert] = idp_cert if fingerprint && doc.validate_document(fingerprint, @soft, opts) if settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) error_msg = "IdP x509 certificate expired" return append_error(error_msg) end end else return append_error(error_msg) end else valid = false expired = false idp_certs[:signing].each do |idp_cert| valid = doc.validate_document_with_cert(idp_cert, true) if valid if settings.security[:check_idp_cert_expiration] if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert) expired = true end end # At least one certificate is valid, restore the old accumulated errors @errors = old_errors break end end if expired error_msg = "IdP x509 certificate expired" return append_error(error_msg) end unless valid # Remove duplicated errors @errors = @errors.uniq return append_error(error_msg) end end true end def name_id_node @name_id_node ||= begin encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID') if encrypted_node node = decrypt_nameid(encrypted_node) else node = xpath_first_from_signed_assertion('/a:Subject/a:NameID') end end end # Extracts the first appearance that matchs the subelt (pattern) # Search on any Assertion that is signed, or has a Response parent signed # @param subelt [String] The XPath pattern # @return [REXML::Element | nil] If any matches, return the Element # def xpath_first_from_signed_assertion(subelt=nil) doc = decrypted_document.nil? ? document : decrypted_document node = REXML::XPath.first( doc, "/p:Response/a:Assertion[@ID=$id]#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION }, { 'id' => doc.signed_element_id } ) node ||= REXML::XPath.first( doc, "/p:Response[@ID=$id]/a:Assertion#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION }, { 'id' => doc.signed_element_id } ) node end # Extracts all the appearances that matchs the subelt (pattern) # Search on any Assertion that is signed, or has a Response parent signed # @param subelt [String] The XPath pattern # @return [Array of REXML::Element] Return all matches # def xpath_from_signed_assertion(subelt=nil) doc = decrypted_document.nil? ? document : decrypted_document node = REXML::XPath.match( doc, "/p:Response/a:Assertion[@ID=$id]#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION }, { 'id' => doc.signed_element_id } ) node.concat( REXML::XPath.match( doc, "/p:Response[@ID=$id]/a:Assertion#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION }, { 'id' => doc.signed_element_id } )) end # Generates the decrypted_document # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted # def generate_decrypted_document if settings.nil? || !settings.get_sp_key raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method') end # Marshal at Ruby 1.8.7 throw an Exception if RUBY_VERSION < "1.9" document_copy = XMLSecurity::SignedDocument.new(response, errors) else document_copy = Marshal.load(Marshal.dump(document)) end decrypt_assertion_from_document(document_copy) end # Obtains a SAML Response with the EncryptedAssertion element decrypted # @param document_copy [XMLSecurity::SignedDocument] A copy of the original SAML Response with the encrypted assertion # @return [XMLSecurity::SignedDocument] The SAML Response with the assertion decrypted # def decrypt_assertion_from_document(document_copy) response_node = REXML::XPath.first( document_copy, "/p:Response/", { "p" => PROTOCOL } ) encrypted_assertion_node = REXML::XPath.first( document_copy, "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)", { "p" => PROTOCOL, "a" => ASSERTION } ) response_node.add(decrypt_assertion(encrypted_assertion_node)) encrypted_assertion_node.remove XMLSecurity::SignedDocument.new(response_node.to_s) end # Decrypts an EncryptedAssertion element # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element # @return [REXML::Document] The decrypted EncryptedAssertion element # def decrypt_assertion(encrypted_assertion_node) decrypt_element(encrypted_assertion_node, /(.*<\/(\w+:)?Assertion>)/m) end # Decrypts an EncryptedID element # @param encryptedid_node [REXML::Element] The EncryptedID element # @return [REXML::Document] The decrypted EncrypedtID element # def decrypt_nameid(encryptedid_node) decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m) end # Decrypts an EncryptedID element # @param encryptedid_node [REXML::Element] The EncryptedID element # @return [REXML::Document] The decrypted EncrypedtID element # def decrypt_attribute(encryptedattribute_node) decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m) end # Decrypt an element # @param encryptedid_node [REXML::Element] The encrypted element # @param rgrex string Regex # @return [REXML::Document] The decrypted element # def decrypt_element(encrypt_node, rgrex) if settings.nil? || !settings.get_sp_key raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') end if encrypt_node.name == 'EncryptedAttribute' node_header = '' else node_header = '' end elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key) # If we get some problematic noise in the plaintext after decrypting. # This quick regexp parse will grab only the Element and discard the noise. elem_plaintext = elem_plaintext.match(rgrex)[0] # To avoid namespace errors if saml namespace is not defined # create a parent node first with the namespace defined elem_plaintext = node_header + elem_plaintext + '' doc = REXML::Document.new(elem_plaintext) doc.root[0] end # Parse the attribute of a given node in Time format # @param node [REXML:Element] The node # @param attribute [String] The attribute name # @return [Time|nil] The parsed value # def parse_time(node, attribute) if node && node.attributes[attribute] Time.parse(node.attributes[attribute]) end end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/attribute_service.rb0000644000004100000410000000301014405647360024107 0ustar www-datawww-datamodule OneLogin module RubySaml # SAML2 AttributeService. Auxiliary class to build the AttributeService of the SP Metadata # class AttributeService attr_reader :attributes attr_reader :name attr_reader :index # Initializes the AttributeService, set the index value as 1 and an empty array as attributes # def initialize @index = "1" @attributes = [] end def configure(&block) instance_eval(&block) end # @return [Boolean] True if the AttributeService object has been initialized and set with the required values # (has attributes and a name) def configured? @attributes.length > 0 && !@name.nil? end # Set a name to the service # @param name [String] The service name # def service_name(name) @name = name end # Set an index to the service # @param index [Integer] An index # def service_index(index) @index = index end # Add an AttributeService # @param options [Hash] AttributeService option values # add_attribute( # :name => "Name", # :name_format => "Name Format", # :index => 1, # :friendly_name => "Friendly Name", # :attribute_value => "Attribute Value" # ) # def add_attribute(options={}) attributes << options end end end end ruby-saml-1.15.0/lib/onelogin/ruby-saml/saml_message.rb0000644000004100000410000001201514405647360023031 0ustar www-datawww-datarequire 'cgi' require 'zlib' require 'base64' require 'nokogiri' require 'rexml/document' require 'rexml/xpath' require "onelogin/ruby-saml/error_handling" # Only supports SAML 2.0 module OneLogin module RubySaml # SAML2 Message # class SamlMessage include REXML ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion".freeze PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol".freeze BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z) @@mutex = Mutex.new # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema # def self.schema @@mutex.synchronize do Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do ::Nokogiri::XML::Schema(File.read("saml-schema-protocol-2.0.xsd")) end end end # @return [String|nil] Gets the Version attribute from the SAML Message if exists. # def version(document) @version ||= begin node = REXML::XPath.first( document, "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest", { "p" => PROTOCOL } ) node.nil? ? nil : node.attributes['Version'] end end # @return [String|nil] Gets the ID attribute from the SAML Message if exists. # def id(document) @id ||= begin node = REXML::XPath.first( document, "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest", { "p" => PROTOCOL } ) node.nil? ? nil : node.attributes['ID'] end end # Validates the SAML Message against the specified schema. # @param document [REXML::Document] The message that will be validated # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not) # @return [Boolean] True if the XML is valid, otherwise False, if soft=True # @raise [ValidationError] if soft == false and validation fails # def valid_saml?(document, soft = true) begin xml = Nokogiri::XML(document.to_s) do |config| config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS end rescue StandardError => error return false if soft raise ValidationError.new("XML load failed: #{error.message}") end SamlMessage.schema.validate(xml).map do |schema_error| return false if soft raise ValidationError.new("#{schema_error.message}\n\n#{xml}") end end private # Base64 decode and try also to inflate a SAML Message # @param saml [String] The deflated and encoded SAML Message # @return [String] The plain SAML Message # def decode_raw_saml(saml, settings = nil) return saml unless base64_encoded?(saml) settings = OneLogin::RubySaml::Settings.new if settings.nil? if saml.bytesize > settings.message_max_bytesize raise ValidationError.new("Encoded SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected") end decoded = decode(saml) begin inflate(decoded) rescue decoded end end # Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding) # @param saml [String] The plain SAML Message # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings # @return [String] The deflated and encoded SAML Message (encoded if the compression is requested) # def encode_raw_saml(saml, settings) saml = deflate(saml) if settings.compress_request CGI.escape(encode(saml)) end # Base 64 decode method # @param string [String] The string message # @return [String] The decoded string # def decode(string) Base64.decode64(string) end # Base 64 encode method # @param string [String] The string # @return [String] The encoded string # def encode(string) if Base64.respond_to?('strict_encode64') Base64.strict_encode64(string) else Base64.encode64(string).gsub(/\n/, "") end end # Check if a string is base64 encoded # @param string [String] string to check the encoding of # @return [true, false] whether or not the string is base64 encoded # def base64_encoded?(string) !!string.gsub(/[\r\n]|\\r|\\n|\s/, "").match(BASE64_FORMAT) end # Inflate method # @param deflated [String] The string # @return [String] The inflated string # def inflate(deflated) Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated) end # Deflate method # @param inflated [String] The string # @return [String] The deflated string # def deflate(inflated) Zlib::Deflate.deflate(inflated, 9)[2..-5] end end end end ruby-saml-1.15.0/UPGRADING.md0000644000004100000410000001474114405647360015423 0ustar www-datawww-data# Ruby SAML Migration Guide ## Updating from 1.12.x to 1.13.0 Version `1.13.0` adds `settings.idp_sso_service_binding` and `settings.idp_slo_service_binding`, and deprecates `settings.security[:embed_sign]`. If specified, new binding parameters will be used in place of `:embed_sign` to determine how to handle SAML message signing (`HTTP-POST` embeds signature and `HTTP-Redirect` does not.) In addition, the `IdpMetadataParser#parse`, `#parse_to_hash` and `#parse_to_array` methods now retrieve `idp_sso_service_binding` and `idp_slo_service_binding`. Lastly, for convenience you may now use the Symbol aliases `:post` and `:redirect` for any `settings.*_binding` parameter. ## Upgrading from 1.11.x to 1.12.0 Version `1.12.0` adds support for gcm algorithm and change/adds specific error messages for signature validations `idp_sso_target_url` and `idp_slo_target_url` attributes of the Settings class deprecated in favor of `idp_sso_service_url` and `idp_slo_service_url`. The `IdpMetadataParser#parse`, `#parse_to_hash` and `#parse_to_array` methods now retrieve SSO URL and SLO URL endpoints with `idp_sso_service_url` and `idp_slo_service_url` (previously `idp_sso_target_url` and `idp_slo_target_url` respectively). ## Upgrading from 1.10.x to 1.11.0 Version `1.11.0` deprecates the use of `settings.issuer` in favour of `settings.sp_entity_id`. There are two new security settings: `settings.security[:check_idp_cert_expiration]` and `settings.security[:check_sp_cert_expiration]` (both false by default) that check if the IdP or SP X.509 certificate has expired, respectively. Version `1.10.2` includes the `valid_until` attribute in parsed IdP metadata. Version `1.10.1` improves Ruby 1.8.7 support. ## Upgrading from 1.9.0 to 1.10.0 Version `1.10.0` improves IdpMetadataParser to allow parse multiple IDPSSODescriptor, Add Subject support on AuthNRequest to allow SPs provide info to the IdP about the user to be authenticated and updates the format_cert method to accept certs with /\x0d/ ## Upgrading from 1.8.0 to 1.9.0 Version `1.9.0` better supports Ruby 2.4+ and JRuby 9.2.0.0. `Settings` initialization now has a second parameter, `keep_security_settings` (default: false), which saves security settings attributes that are not explicitly overridden, if set to true. ## Upgrading from 1.7.x to 1.8.0 On Version `1.8.0`, creating AuthRequests/LogoutRequests/LogoutResponses with nil RelayState param will not generate a URL with an empty RelayState parameter anymore. It also changes the invalid audience error message. ## Upgrading from 1.6.0 to 1.7.0 Version `1.7.0` is a recommended update for all Ruby SAML users as it includes a fix for the [CVE-2017-11428](https://www.cvedetails.com/cve/CVE-2017-11428/) vulnerability. ## Upgrading from 1.5.0 to 1.6.0 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. 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 of Microsoft ADFS, so messages from ADFS can fail signature validation. The new preferred way to provide _SAMLResponse_, _RelayState_, and _SigAlg_ is via the `options[:raw_get_params]` parameter. For example: ```ruby # In this example `query_params` is assumed to contain decoded query parameters, # and `raw_query_params` is assumed to contain encoded query parameters as sent by the IDP. settings = { settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1 settings.soft = false } options = { get_params: { "Signature" => query_params["Signature"], }, raw_get_params: { "SAMLRequest" => raw_query_params["SAMLRequest"], "SigAlg" => raw_query_params["SigAlg"], "RelayState" => raw_query_params["RelayState"], }, } slo_logout_request = OneLogin::RubySaml::SloLogoutrequest.new(query_params["SAMLRequest"], settings, options) raise "Invalid Logout Request" unless slo_logout_request.is_valid? ``` The old form is still supported for backward compatibility, but all Ruby SAML users should prefer `options[:raw_get_params]` where possible to ensure compatibility with other SAML implementations. ## Upgrading from 1.4.2 to 1.4.3 Version `1.4.3` introduces Recipient validation of SubjectConfirmation elements. The 'Recipient' value is compared with the settings.assertion_consumer_service_url value. If you want to skip that validation, add the :skip_recipient_check option to the initialize method of the Response object. Parsing metadata that contains more than one certificate will propagate the idp_cert_multi property rather than idp_cert. See [signature validation section](#signature-validation) for details. ## Upgrading from 1.3.x to 1.4.x Version `1.4.0` is a recommended update for all Ruby SAML users as it includes security improvements. ## Upgrading from 1.2.x to 1.3.x 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](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5697) ## Upgrading from 1.1.x to 1.2.x 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 [CHANGELOG.md](CHANGELOG.md). ## Upgrading from 1.0.x to 1.1.x Version `1.1` adds some improvements on signature validation and solves some namespace conflicts. ## Upgrading from 0.9.x to 1.0.x 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. ### Important Changes 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. ## Upgrading from 0.8.x to 0.9.x Version `0.9` adds many new features and improvements. ## Upgrading from 0.7.x to 0.8.x Version `0.8.x` changes the namespace of the gem from `OneLogin::Saml` to `OneLogin::RubySaml`. Please update your implementations of the gem accordingly. ruby-saml-1.15.0/Gemfile0000644000004100000410000000013614405647360015045 0ustar www-datawww-data# # Please keep this file alphabetized and organized # source 'https://rubygems.org' gemspec ruby-saml-1.15.0/.github/0000755000004100000410000000000014405647360015112 5ustar www-datawww-dataruby-saml-1.15.0/.github/workflows/0000755000004100000410000000000014405647360017147 5ustar www-datawww-dataruby-saml-1.15.0/.github/workflows/test.yml0000644000004100000410000000225414405647360020654 0ustar www-datawww-dataname: ruby-saml CI on: [push, pull_request] jobs: test: name: Unit test strategy: fail-fast: false matrix: os: [ubuntu-20.04, macos-latest] ruby-version: [2.1.9, 2.2.10, 2.3.8, 2.4.6, 2.5.8, 2.6.6, 2.7.2, 3.0.1, 3.1, 3.2, jruby-9.1.17.0, jruby-9.2.17.0, jruby-9.3.2.0, jruby-9.4.0.0, truffleruby] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} - name: Install dependencies run: bundle install - name: Run tests run: bundle exec rake - name: Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} parallel: true flag-name: run-${{ matrix.ruby-version }} finish: needs: test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} flag-name: run-${{ matrix.ruby-version }} parallel-finished: true ruby-saml-1.15.0/ruby-saml.gemspec0000644000004100000410000000734414405647360017042 0ustar www-datawww-data$LOAD_PATH.push File.expand_path('../lib', __FILE__) require 'onelogin/ruby-saml/version' Gem::Specification.new do |s| s.name = 'ruby-saml' s.version = OneLogin::RubySaml::VERSION s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["SAML Toolkit", "Sixto Martin"] s.email = ['contact@iamdigitalservices.com', 'sixto.martin.garcia@gmail.com'] s.date = Time.now.strftime("%Y-%m-%d") s.description = %q{SAML Ruby toolkit. Add SAML support to your Ruby software using this library} s.license = 'MIT' s.extra_rdoc_files = [ "LICENSE", "README.md" ] s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } s.homepage = %q{https://github.com/saml-toolkit/ruby-saml} s.rdoc_options = ["--charset=UTF-8"] s.require_paths = ["lib"] s.rubygems_version = %q{1.3.7} s.required_ruby_version = '>= 1.8.7' s.summary = %q{SAML Ruby Tookit} # Because runtime dependencies are determined at build time, we cannot make # Nokogiri's version dependent on the Ruby version, even though we would # have liked to constrain Ruby 1.8.7 to install only the 1.5.x versions. if defined?(JRUBY_VERSION) if JRUBY_VERSION < '9.1.7.0' s.add_runtime_dependency('nokogiri', '>= 1.8.2', '<= 1.8.5') s.add_runtime_dependency('jruby-openssl', '>= 0.9.8') s.add_runtime_dependency('json', '< 2.3.0') elsif JRUBY_VERSION < '9.2.0.0' s.add_runtime_dependency('nokogiri', '>= 1.9.1', '< 1.10.0') elsif JRUBY_VERSION < '9.3.2.0' s.add_runtime_dependency('nokogiri', '>= 1.11.4') s.add_runtime_dependency('rexml') else s.add_runtime_dependency('nokogiri', '>= 1.13.10') s.add_runtime_dependency('rexml') end elsif RUBY_VERSION < '1.9' s.add_runtime_dependency('uuid') s.add_runtime_dependency('nokogiri', '<= 1.5.11') elsif RUBY_VERSION < '2.1' s.add_runtime_dependency('nokogiri', '>= 1.5.10', '<= 1.6.8.1') s.add_runtime_dependency('json', '< 2.3.0') elsif RUBY_VERSION < '2.3' s.add_runtime_dependency('nokogiri', '>= 1.9.1', '< 1.10.0') elsif RUBY_VERSION < '2.5' s.add_runtime_dependency('nokogiri', '>= 1.10.10', '< 1.11.0') s.add_runtime_dependency('rexml') elsif RUBY_VERSION < '2.6' s.add_runtime_dependency('nokogiri', '>= 1.11.4') s.add_runtime_dependency('rexml') else s.add_runtime_dependency('nokogiri', '>= 1.13.10') s.add_runtime_dependency('rexml') end s.add_development_dependency('simplecov', '<0.22.0') if RUBY_VERSION < '2.4.1' s.add_development_dependency('simplecov-lcov', '<0.8.0') else s.add_development_dependency('simplecov-lcov', '>0.7.0') end s.add_development_dependency('minitest', '~> 5.5') s.add_development_dependency('mocha', '~> 0.14') if RUBY_VERSION < '2.0' s.add_development_dependency('rake', '~> 10') else s.add_development_dependency('rake', '>= 12.3.3') end s.add_development_dependency('shoulda', '~> 2.11') s.add_development_dependency('systemu', '~> 2') if RUBY_VERSION < '2.1' s.add_development_dependency('timecop', '<= 0.6.0') else s.add_development_dependency('timecop', '~> 0.9') end if defined?(JRUBY_VERSION) # All recent versions of JRuby play well with pry s.add_development_dependency('pry') elsif RUBY_VERSION < '1.9' # 1.8.7 s.add_development_dependency('ruby-debug', '~> 0.10.4') elsif RUBY_VERSION < '2.0' # 1.9.x s.add_development_dependency('debugger-linecache', '~> 1.2.0') s.add_development_dependency('debugger', '~> 1.6.4') elsif RUBY_VERSION < '2.1' # 2.0.x s.add_development_dependency('byebug', '~> 2.1.1') else # 2.1.x, 2.2.x s.add_development_dependency('pry-byebug') end end