Code Monkey home page Code Monkey logo

ruby-saml's Introduction

Ruby SAML

ruby-saml CI Coverage Status Rubygem Version GitHub version GitHub Gem Gem

Ruby SAML minor and tiny versions may introduce breaking changes. Please read UPGRADING.md for guidance on upgrading to new Ruby SAML versions.

There is a critical vulnerability affecting ruby-saml < 1.17.0 (CVE-2024-45409). Make sure you are using an updated version. (1.12.3 is safe)

Overview

The Ruby SAML library is for implementing the client side of a SAML authorization, i.e. it provides a means for managing authorization initialization and confirmation requests from identity providers.

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

Supported Ruby Versions

The following Ruby versions are covered by CI testing:

  • Ruby (MRI) 2.1 to 3.3
  • JRuby 9.1 to 9.4
  • TruffleRuby (latest)

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.

Security Guidelines

If you believe you have discovered a security vulnerability in this gem, please report it by mail to the maintainer: [email protected]

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

# latest stable
gem 'ruby-saml', '~> 1.11.0'

# or track master for bleeding-edge
gem 'ruby-saml', :github => 'saml-toolkit/ruby-saml'

Using RubyGems

gem install ruby-saml

You may require the entire Ruby SAML gem:

require 'onelogin/ruby-saml'

or just the required components individually:

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

gem 'nokogiri', '~> 1.5.10'

Using RubyGems

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:

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):

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:

def init
  request = OneLogin::RubySaml::Authrequest.new
  saml_settings.name_identifier_value_requested = "[email protected]"
  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):

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).

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:

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:

# 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.

{
  :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.

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:

  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"
             )

Retrieve one Entity Descriptor with an specific binding and nameid format when several are available

If the Metadata contains several bindings and nameids, the relevant ones also can be specified when retrieving the settings from the IdpMetadataParser by the values of binding and nameid:

  validate_cert = true
  options = {
    entity_id: "http//example.com/target/entity",
    name_id_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
    sso_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
    slo_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
  }
  settings = idp_metadata_parser.parse_remote(
               "https://example.com/auth/saml2/idp/metadata",
               validate_cert,
               options
             )

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, 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 = "<url_to_the_metadata>"
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 = "<include_cert_here>"
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: "<entity_id_of_the_entity_to_be_retrieved>"
  )
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:AttributeStatements with its 'Name' as an indifferent key and one or more saml:AttributeValues as values. The value returned depends on the value of the single_value_compatibility (when activated, only the first value is returned)

response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

response.attributes[:username]

Imagine this saml:AttributeStatement

  <saml:AttributeStatement>
    <saml:Attribute Name="uid">
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">demo</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="another_value">
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">value1</saml:AttributeValue>
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">value2</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="role">
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role1</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="role">
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role2</saml:AttributeValue>
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">role3</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="attribute_with_nil_value">
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
    </saml:Attribute>
    <saml:Attribute Name="attribute_with_nils_and_empty_strings">
      <saml:AttributeValue/>
      <saml:AttributeValue>valuePresent</saml:AttributeValue>
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>
      <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="1"/>
    </saml:Attribute>
    <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
      <saml:AttributeValue>usersName</saml:AttributeValue>
    </saml:Attribute>
  </saml:AttributeStatement>
pp(response.attributes)   # is an OneLogin::RubySaml::Attributes object
# => @attributes=
  {"uid"=>["demo"],
   "another_value"=>["value1", "value2"],
   "role"=>["role1", "role2", "role3"],
   "attribute_with_nil_value"=>[nil],
   "attribute_with_nils_and_empty_strings"=>["", "valuePresent", nil, nil]
   "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.

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:

  # 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:

  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):

  settings.security[:digest_method]    = XMLSecurity::Document::SHA1
  settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1

Signing SP Metadata

You may add a <ds:Signature> digital signature element to your SP Metadata XML using the following setting:

  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 <md:KeyDescriptor use="signing"> to your SP Metadata XML, to be read by the IdP.

  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:

  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 <md:KeyDescriptor use="encryption"> to your SP Metadata XML, to be read by the IdP.

  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 <md:SPSSODescriptor WantAssertionsSigned="true"> to your SP Metadata XML. The signature will be checked against the <md:KeyDescriptor use="signing"> element present in the IdP's metadata.

  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:

  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.

  settings.security[:soft] = true  # Do not raise error on failed signature/certificate validations

Advanced SP Certificate Usage & Key Rollover

Ruby SAML provides the settings.sp_cert_multi parameter to enable the following advanced usage scenarios:

  • Rotating SP certificates and private keys without disruption of service.
  • Specifying separate SP certificates for signing and encryption.

The sp_cert_multi parameter replaces certificate and private_key (you may not specify both pparameters at the same time.) sp_cert_multi has the following shape:

settings.sp_cert_multi = {
  signing: [
    { certificate: cert1, private_key: private_key1 },
    { certificate: cert2, private_key: private_key2 }
  ],
  encryption: [
    { certificate: cert1, private_key: private_key1 },
    { certificate: cert3, private_key: private_key1 }
  ],
}

Certificate rotation is acheived by inserting new certificates at the bottom of each list, and then removing the old certificates from the top of the list once your IdPs have migrated. A common practice is for apps to publish the current SP metadata at a URL endpoint and have the IdP regularly poll for updates.

Note the following:

  • You may re-use the same certificate and/or private key in multiple places, including for both signing and encryption.
  • The IdP should attempt to verify signatures with all :signing certificates, and permit if any one succeeds. When signing, Ruby SAML will use the first SP certificate in the sp_cert_multi[:signing] array. This will be the first active/non-expired certificate in the array if settings.security[:check_sp_cert_expiration] is true.
  • The IdP may encrypt with any of the SP certificates in the sp_cert_multi[:encryption] array. When decrypting, Ruby SAML attempt to decrypt with each SP private key in sp_cert_multi[:encryption] until the decryption is successful. This will skip private keys for inactive/expired certificates if :check_sp_cert_expiration is true.
  • If :check_sp_cert_expiration is true, the generated SP metadata XML will not include inactive/expired certificates. This avoids validation errors when the IdP reads the SP metadata.

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:

<AudienceRestriction>
  <Audience />
</AudienceRestriction>

You may enforce that an element containing only empty elements is invalid using the settings.security[:strict_audience_validation] parameter.

settings.security[:strict_audience_validation] = true

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:

# 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:

# 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:

# 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:

# 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).

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:

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:

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.

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:

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 = '[email protected]'
  end
end

# Output XML with custom metadata
MyMetadata.new.generate(settings)

ruby-saml's People

Contributors

alagos avatar billyyarosh avatar bonyiii avatar borgand avatar buffym avatar calh avatar d3mcfadden avatar davidlibrera avatar jcb-k avatar jeffparsons avatar johnnyshields avatar lawrencepit avatar lordnibbler avatar morten avatar oc avatar olleolleolle avatar phene avatar phlipper avatar pitbulk avatar rafaelgonzalez avatar shyam-habarakada avatar soupmatt avatar sthanson avatar stouset avatar tkalliom avatar tknzk avatar umofomia avatar vincentwoo avatar withshubh avatar ylansegal avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ruby-saml's Issues

How do I log out?

Which URL do I need to get to in order to log out?

I am using omniauth-saml with devise. This is how my routes.rb look:

constraints(subdomain: /.+/) do
    devise_for :users, path_names: {sign_in: 'auth/saml', sign_out: 'auth/saml/logout'},
                       controllers: {omniauth_callbacks: 'auth/omniauth_callbacks'}
    get '/users/auth/:provider/setup' => 'auth/saml#setup'
    get '/' => 'dashboard#index'
  end

But, when I go to https://xyz.mysite.dev/users/auth/saml/logout I get No route matches [GET] "/users/auth/saml/logout"

I have made this modification in devise.rb initializer:

config.sign_out_via = :get

Times contain prohibited time-zone component

Times that are generated using ruby-saml include a time-zone component (always Z), but by my reading of SAMLCore this is prohibited in section 1.3.3 - Time Values:

"All SAML time values have the type xs:dateTime, which is built in to the W3C XML Schema Datatypes
312 specification [Schema2], and MUST be expressed in UTC form, with no time zone component." (emphasis mine)

Do people disagree with my interpretation of this - it seems pretty clear to me...

test failure for xml_security_test

The test xml_security_test.rb contains no assertions. I added an assertion for validate_doc as follows on line 13:

  assert @document.validate_doc(base64cert, nil)

And I receive the following error. I am running Ruby 1.8.7 P330. I forked and commited the change to git://github.com/ekolve/ruby-saml.git

1) Failure:
test: XmlSecurity should should provide getters and settings. (XmlSecurityTest)
[./test/xml_security_test.rb:13:in `__bind_1296505308_249038'
 /usr/lib/ruby/gems/1.8/gems/shoulda-2.11.3/lib/shoulda/context.rb:382:in `call'
 /usr/lib/ruby/gems/1.8/gems/shoulda-2.11.3/lib/shoulda/context.rb:382:in `test: XmlSecurity should should provide getters and settings. '
 /usr/lib/ruby/gems/1.8/gems/mocha-0.9.10/lib/mocha/integration/test_unit/ruby_version_186_and_above.rb:22:in `__send__'
 /usr/lib/ruby/gems/1.8/gems/mocha-0.9.10/lib/mocha/integration/test_unit/ruby_version_186_and_above.rb:22:in `run']:
<false> is not true.

The result of xml_security_test.rb is failed

I just print the result of @document.validate_doc(...) in xml_security_test.rb, however the result is "false".
So is the signature verification failed or there is something wrong with the test request?

Code in xml_security_test.rb:
should "should run validate without throwing NS related exceptions" do
base64cert = @document.elements["//ds:X509Certificate"].text
puts @document.validate_doc(base64cert, nil)
end

README.rdoc issue

Simple change in Service Provider Metadata section:

render :xml => meta.create(settings)

should read

render :xml => meta.generate(settings)

Consistent travis build failures

So each build that's triggered through travis is failing consistently at the minute, due to a loose dependency on the shoulda-matchers gem. (An example failure output: https://travis-ci.org/onelogin/ruby-saml/jobs/7745319 )

Basically, it's failing due to the latest shoulda-matchers gem now requiring 1.9.2, and ruby-saml just depending on the latest shoulda-matchers gem blindly. Couple that to travis being set to test on 1.8.7 and ree (which is also 1.8.7 compatible), and 2/3 of the ruby versions tested fail during gem installation. Not ideal!

The way I see it there's a couple of solutions. We could stop testing on 1.8.7 rubies, as it's end of life even for security fixes this month, June 2013. Or we could tighten the dependency on shoulda-matchers version so it still installs under 1.8.7 rubies.

Personally, I think I'd rather see 1.8.7 dropped from being tested against as it's EOL, and add ruby 2.0.0 in there instead. And on that note, I can see there's a pull request (#82) to add jruby & ruby 2.0.0 support, which drops 1.8.7 from being tested. Merging that would solve this issue!

SP metadata

Hi there, firstly thank you so much for creating this gem! I know I'd be up the river without it! :)

I'm trying to implement the New Zealand Ministry of Education's SSO requirements for my rails app. They are using SAML2 and provide limited support to third parties when it comes to getting it set up. I've gotten to this point with them and can't see a way to generate this information for him using ruby-saml:

I still need you to give me your SP metadata for your locally running SP end point. How that is created must be a feature of the software libraries you are using, so you need to contact the developers of it to get assistance with that part. Once you have this information, I can import it into the idp.catalyst.net.nz test IdP, and ensure that the communication is functioning correctly.

Any help on the matter would be much appreciated :)

[Single Logout Support] Send a LogoutResponse

I've been trying to integrate ruby-saml with a rails project of mine, but I need to be able to support a Single Logout scenario which, as far as I could see, the library still doesn't support.

I'm referring to a scenario in which a rails project using ruby-saml initiates an IDP session, which is then used by another SP. After a while, this second SP tries to initiate the log out procedure. According to the ruby-saml protocol, the IDP will have to send a LogoutRequest to every single SP currently logged in, except the one that initiated the Logout procedure (the rails project). This means that our ruby-saml rails application will receive a LogoutRequest and it will have to be able to send back a LogoutResponse, before the IDP actually logs out and sends back a Logout Response to the initial SP.

Is there any way to send a LogoutResponse in the current version of this library?
I've checked calh's fork and this is possible on that version, which is the reason why I'm using it currently. I've noticed that some of his code was added to this library, but I don't think that functionality is present yet, but I could be mistaken.

Thanks for your work. ;)

SAML POST Binding Support in AuthNRequest

This is more of a general question on how you would handle POST binding to the provider with ruby-saml. I see in the docs the Redirect binding is clearly supported and documented:

def init
  request = OneLogin::RubySaml::Authrequest.new
  redirect_to(request.create(saml_settings))
end

I have a need to support the POST binding support and have written some code to do that. There is not anything to currently do this in this lib right?

ruby-saml doesn't work with ADFS2 server

We are trying to use your library to auth with ADFS2 server and validation permanent fails with digest mismatch.

Commited 5 day ago "Fixing XMLSecurity so it works with the StarfieldTMS IdP" by bundacia haven't resolved this issue.

We are trying to run tests and they are failing. Using jruby 1.7.0 (1.9.3p203)

Need ProtocolBinding attribute in auth request root

A SAML provider is telling us we must add

ProtocolBinding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'

in the root of the AuthnRequest. We work with a bunch of providers that do not require this, but this one is telling us they require it. I can see how I would go about adding that into the gem, so I'd be glad to do that and issue a pull request. Before I do that, I just wanted to ask if this is considered a valid SAML requirement, and if it's something you'd consider merging.

Please release ruby-saml 4.8 gem with metadata.rb

Hi,

Thank you for your development of ruby-saml.
I am using your product in my code rack-saml and it is basically released through RubyGems.
Currently, I have an issue that I can not use Onelogin::Saml::Metadata in RubyGems version of ruby-saml 4.7.

toyokazu/rack-saml#2

If possible, could you release ruby-saml 4.8 with onelogin/saml/metadata.rb.

Best Regards

SHA256 DigestMethod

I'm receiving the following:

<ds:DigestMethod Algorithm='http://www.w3.org/2001/04/xmlenc#sha256'/>
<ds:DigestValue>VixjWsKWEdVCCfEJaAjYRNpORKxPSUx1lHUOyoPbl7M=</ds:DigestValue>

this doesn't pass in method validate_doc as the hash calculated in that method assumes the method is always SHA1.

Multi Attributes error in SAML parsing

The following private function creates an issue parsing SAML XML that represents multi-valued fields.

      def xpath_first_from_signed_assertion(subelt=nil)
        node = REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION })
        node ||= REXML::XPath.first(document, "/p:Response[@ID='#{document.signed_element_id}']/a:Assertion#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION })
        node
      end

When the SAML looks like the XML below, you will only see "A" in the ruby hash, and never see the B and C values.

Need to provide a means of XML node access that doesn't use the .first idiom.

REXML::XPath.first

<saml:Attribute FriendlyName="CustomParamArray"
                Name="CustomParamArray"
                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
    <saml:AttributeValue xsi:type="xs:string">A</saml:AttributeValue>
    <saml:AttributeValue xsi:type="xs:string">B</saml:AttributeValue>
    <saml:AttributeValue xsi:type="xs:string">C</saml:AttributeValue>
</saml:Attribute>

uninitialized constant XMLSecurity::SignedDocument::XPath

While trying to validate a SAML Response I received the following error:
/usr/lib/ruby/gems/1.8/gems/ruby-saml-0.2.1/lib/xml_sec.rb:55:in validate_doc': uninitialized constant XMLSecurity::SignedDocument::XPath (NameError) from /usr/lib/ruby/gems/1.8/gems/ruby-saml-0.2.1/lib/xml_sec.rb:48:invalidate'
from /usr/lib/ruby/gems/1.8/gems/ruby-saml-0.2.1/lib/onelogin/saml/response.rb:21:in `is_valid?'

I'm running Ruby 1.8.7 p302 with ruby-saml 0.2.1. What seemed to fix this was when I prepended all instances of 'XPath' with 'REXML::'.

Support AttributeConsumingServiceIndex in AuthnRequest

As far as I can tell it currently isn't possible to set AttributeConsumingServiceIndex=some_value for an AuthnRequest, as I only see a mention of AttributeConsumingServiceIndex in the saml20protocol_schema.xsd.

I guess this could be implemented in a manner similar to IsPassive in 8a825f3 or #92 ?

PingIdentity's PingFederate isn't compatible?

I've been trying to get this version of the plugin working with Ping Identity's Ping Federate locally (we are currently running an older version of this plugin which worked fine, and need to upgrade to support another integration) - however this latest version seems to be incompatible.

The problems seem to be all happening in XmlSecurity - Firstly, I had to change:

base64_cert = self.elements["//ds:X509Certificate"].text  #line 47 

to

base64_cert = REXML::XPath.first(self, "//ds:X509Certificate").text #line 47 

As REXML was having trouble with the XML which was generated - this got the document created correctly and doesn't change the meaning of the code overmuch.

Howevever, the primary problem now is that the digest_hash doesn't match the calculated hash value of the SAML Response. I think that this might be because attributes are being used, and it could be related to the previously reported issue #16.

The digest method and canonicalization method are unchanged, the digests simply just don't match up. I'm looking further into the problem just now to see if there's a way around this - has anyone encountered a similar problem?

is there anything about RelayState?

I send to SSO provider a param, and want the provider send the param without any modifiion, how could I do that? will RelayState be a option?

License missing from gemspec

RubyGems.org doesn't report a license for your gem. This is because it is not specified in the gemspec of your last release.

via e.g.

spec.license = 'MIT'
# or
spec.licenses = ['MIT', 'GPL-2']

Including a license in your gemspec is an easy way for rubygems.org and other tools to check how your gem is licensed. As you can imagine, scanning your repository for a LICENSE file or parsing the README, and then attempting to identify the license or licenses is much more difficult and more error prone. So, even for projects that already specify a license, including a license in your gemspec is a good practice. See, for example, how rubygems.org uses the gemspec to display the rails gem license.

There is even a License Finder gem to help companies/individuals ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough issue that even Bundler now generates gems with a default 'MIT' license.

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue with a nice message. In either case, I'll follow up. Thanks for your time!

Appendix:

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), GitHub has created a license picker tool. Code without a license specified defaults to 'All rights reserved'-- denying others all rights to use of the code.
Here's a list of the license names I've found and their frequencies

p.s. In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :). See the previous link or my blog post about this project for more information.

Exclusive XML Canonicalization 1.0 is not supported

Exclusive XML Canonicalization 1.0 (exc_c14n) is not supported. See: http://www.w3.org/TR/xml-exc-c14n/

Unfortunately, I have not found any ruby library that supports Exclusive XML Canonicalization. However, the libxml2 package does have some (not very well documented) support for exc_c14n. Unfortunately, (again) libxml2-ruby doesn't expose exc_c14n in its canonicalize method.

As a first step I would prefer that an exception is raised (instead of the current silent ignore) for Assertions that include InclusiveNamespaces with PrefixList.

The best solution might be to add exc_c14n to libxml2-ruby and then switch to using libxml2-ruby instead of canonix.

saml2:EncryptedAssertion

Hi,

the answer I get from the (Shibboleth) SAML 2 IdP I'm talking to is completely encoded and wrapped in a saml2:EncryptedAssertion element containing a certificate and two ciphers. Ruby-saml doesn't know how to handle this yet and I have not yet found out, how to handle it either.

Is anybody of you familiar with this type of response and perhaps already working on adding this to ruby-saml?

Also, any pointers are much appreciated so I can add it on my own.

Best,
Chris

Problem verifying integrity of signature from simplesamlphp

Hi there,

I'm getting mis-matched hashes when trying to verify a signature generated with a simplesamlphp server that we're trying to integrate with. We have no access to that server, so can't debug from that end. Essentially the hashes don't match up at the end of the code segment below. I've checked and the incoming XML specify's that their hash was generated with SHA1. I'm wondering if anyone could give me some pointers on how to troubleshoot this kind of problem? Obviously there must be a difference in the canonicalised markup on each side.

  REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do | ref |

    uri                   = ref.attributes.get_attribute("URI").value
    hashed_element        = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']")
    canoner               = XML::Util::XmlCanonicalizer.new(false, true)
    canon_hashed_element  = canoner.canonicalize(hashed_element)
    hash                  = Base64.encode64(Digest::SHA1.digest(canon_hashed_element)).chomp
    digest_value          = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text

    valid_flag            = hash == digest_value

    return valid_flag if !valid_flag
  end

Namespaces in elements

The root element should carry the namespace definition.
For example the following authnrequest created by the toolkit:

<samlp:AuthnRequest ID='_c73ac510-1a71-0132-27c0-0090f5dedd77'
                    Version='2.0'
                    xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'                    
                    AssertionConsumerServiceURL='http://rubysaml.com:3000/saml/acs'
                    Destination='https://app.onelogin.com/trust/saml2/http-post/sso/396421'
                    IssueInstant='2014-09-09T17:08:01Z'>
    <saml:Issuer xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>         http://rubysaml.com:3000/saml/metadata</saml:Issuer>
    <samlp:NameIDPolicy AllowCreate='true' 
                        Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'         
                        xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol' />
</samlp:AuthnRequest>

should be

<samlp:AuthnRequest ID='_c73ac510-1a71-0132-27c0-0090f5dedd77'
                    Version='2.0'
                    xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'
                    xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'
                    AssertionConsumerServiceURL='http://rubysaml.com:3000/saml/acs'
                    Destination='https://app.onelogin.com/trust/saml2/http-post/sso/396421'
                    IssueInstant='2014-09-09T17:08:01Z'>
    <saml:Issuer>http://rubysaml.com:3000/saml/metadata</saml:Issuer>
    <samlp:NameIDPolicy AllowCreate='true' 
                        Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' />
</samlp:AuthnRequest>

Ruby-SAML/SimpleSAML - validation fails when attributes are enabled

Note: I am using canonix for xml canonicalization.

When I have attributes enabled for a service provider validation fails in Ruby-SAML when validating the SAML response. I have tested using SimpleSAML as an SP to connect to my SimpleSAML IdP and validation is successful, as it is with every other service provider I have enabled. The only one that is failing is the Ruby app using Ruby SAML.

Admittedly I don't know too much about PHP so it is somewhat difficult to track down what is causing the issue. I'm just hoping that perhaps someone else has ran into this same issue and has some insight as to what is going on and may have a suggestion for either a fix or a potential work around.

NotBefore causing troubles when server times slightly out of sync

Authentication between an ADFS v2 server and ruby-saml seems to cause problems when there is a slight difference in server time. If the ADFS server's time is a bit ahead, the NotBefore field of the signature appears in the future to ruby-saml and is thus invalid.
This should probably ideally be fixed in some ADFS settings, but they are not very obvious/hard to find. To prevent issues like these where a 1 second time difference can already cause problems, a solution may be to embed a NotBeforeSkew (optional or maybe even by default of a couple of minutes), which allows the NotBefore time to be a bit the future?

What do you think?

Remove dependency on abandoned xmlcanonicalizer gem

It appears that the xmlcanonicalizer gem that is depended upon by ruby-saml has a fairly serious bug that makes it impossible to verify some XML message digests. The team at relevance have fixed the problem in a fork and submitted a patch but it appears that the gem maintainer isn't interested in maintaining this gem anymore.

What does everyone think about creating a new xml canonicalising gem for ruby-saml to depend upon? Perhaps onelogin could host this?

The proposed patch is here: andrewferk/xmlcanonicalizer#1

I've tested it and it works well at least for my use case.

Relevance have also made a lot of improvements to the ruby-saml gem which would probably be worthwhile integrating into the master branch. On first ask, they appear to be open to the idea of pushing these up.

Don't name controller action 'initialize'

Spent a few hours fiddling before figuring this out. On rails 2 this is pretty catastrophic and causes very weird issues. Name the action 'init' or 'start' or really anything but the class constructor method name.

require wrong path at xml_security.rb (version 0.5.1)

Sorry, this is re-post from closed issue #27

Thank you for your update from 0.4.7 to 0.5.1!

With the new version 0.5.1, when I use Onelogin::Saml::Metadata, I've got the following error

LoadError (cannot load such file -- onelogin/saml/validation_error):

I guess that it is caused by the require method in xml_security.rb.

% vi lib/xml_security.rb
require "onelogin/saml/validation_error"

It should be "onelogin/ruby-saml/validation_error" for the new source tree, right?

SAML IdP

I need to write a SAML Identity Provider (IdP) in Ruby. Can I do that with this Gem?

nokogiri 1.5 dependency breaks other users

The new 1.5 enforcement causes bundle to choose 1.5 now, when it had been using 1.6 for quite some time. Can you put the >= 1.5 back and if a constant is required, use a different solution to find it?

NameIDPolicy

I have been trying for days to get this to work with AD FS 2.0. No matter what I do the request sent to AD shows the following..

<samlp:AuthnRequest AssertionConsumerServiceURL='https://test.mindsmesh.com:3000/users/auth/saml/callback' Destination='https://dc2012mm.mindsmesh.com/adfs/ls/IdpInitiatedSignon.aspx' ID='_4b0607d0-a553-0130-0f40-2820663a1040' IssueInstant='2013-05-22T21:20:04Z' Version='2.0' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'><saml:Issuer xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>https://dc2012mm.mindsmesh.com/adfs/services/trust/saml:Issuer<samlp:NameIDPolicy AllowCreate='true' Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'/>/samlp:AuthnRequest

However AD always sees the NameIDPolicy as null. I think onelogin is forming the SAML request wrong.

should be

<samlp:NameIDPolicy xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol' AllowCreate='true' Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' />

Thoughts?

Chad

XMLSecurity::SignedDocument.validate not recognizing X509Certificate text

I had this problem with two different providers. In def validate:

base64_cert = self.elements["//ds:X509Certificate"].text

returned an error because the ds:X509Certificate node was nil.

I changed it to

base64_cert = REXML::XPath.first(self, "//ds:X509Certificate", {"ds"=>DSIG}).text

and it was able to find the node and return the proper text.

Signed Request

Is there any way to generate a signed request?

I see there was one pull request to add support for signed requests which was rejected for other reasons.

response.validate! call fails

Hi,

I am trying to use ruby-saml with a custom OmniAuth Strategy. I am using version 0.7.2
In my heroku logs I see

NoMethodError (undefined method `text' for nil:NilClass):

Below is what my OmniAuth callback phase looks like. The failure seems to be around the response.validate! call. Any thoughts on why this is happening? Problem in my code or in ruby-saml? I know I can receive the SAML response if I comment out that

response.validate! 

call. But I am trying to understand the validate method. And specifically what steps I need to take to ensure the SAML assertion my application is receiving is properly signed and trustworthy.

      def callback_phase
        unless request.params['SAMLResponse']
          raise OmniAuth::Strategies::Company::ValidationError.new("SAML response missing")
        end

        response = Onelogin::Saml::Response.new(request.params['SAMLResponse'])
        response.settings = Onelogin::Saml::Settings.new(options)
        @name_id = response.name_id
        @attributes = response.attributes

        if @name_id.nil? || @name_id.empty?
          raise OmniAuth::Strategies::Company::ValidationError.new("SAML response missing 'name_id'")
        end
        response.validate!
        super
      rescue OmniAuth::Strategies::Company::ValidationError
        fail!(:invalid_ticket, $!)
      rescue Onelogin::Saml::ValidationError
        fail!(:invalid_ticket, $!)
      end

      uid { @name_id }
      info do
        {
            :company_id  => @attributes[:"CORP ID"],
            :name  => @attributes[:"Given Name"],
            :email  => @attributes[:"Email Address"],
            :account_type  => @attributes[:"Account Type"],
        }
      end

Request For Moderation

If the core team at OneLogin is too busy, would you guys be ok with handing moderatorship to another contributor? The speed of PR acceptance is a bit slow and I know much of the community would benefit from including many of the open PRs.

Encrypted response

Seems like this gem used to handle encrypted responses but got dropped along the way.

Any change it will come back or am I overlooking something? This version is validating the response way better than the old version, a combination of those too seems solid.

Support for ForceAuthn AuthnRequest

As far as I can tell it currently isn't possible to set ForceAuthn=true for an AuthnRequest, as I only see a mention of ForceAuthn in the saml20protocol_schema.xsd.

I guess this could be implemented in a manner similar to IsPassive in 8a825f3 ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.