ruby-saml Documentation

repository·master·Indexed 21 days ago

https://github.com/saml-toolkits/ruby-saml

A Ruby library for implementing the client side of SAML authorization. It provides tools to manage authorization initialization and confirmation requests from Identity Providers (IdPs), including support for SAML response validation, attribute extraction, SP metadata generation, and IdP metadata parsing via OneLogin::RubySaml.

Tokens
19.3K
Snippets
58
Records
81
Agent score
76%

What's inside ruby-saml

  1. Security considerations for Ruby SAML

    master

    When implementing SAML with this library, keep the following security responsibilities in mind:

    • IdP Metadata URL Validation: Ruby SAML does not validate if a supplied IdP Metadata URL is correct or safe. You must ensure the URL is from a trusted source.
    • Replay Attack Mitigation: While the library provides tools to help, it is the developer's responsibility to implement the logic required to prevent attackers from reusing intercepted SAML assertions.
    • Nokogiri Security: You may see security warnings regarding the Nokogiri dependency. These can be ignored; Ruby SAML uses Nokogiri safely by disabling DTDLOAD and enabling NONET options.
  2. Validate SAML signatures

    master

    You can validate the IdP's signature using one of two methods:

    1. X.509 Certificate: Provide the full certificate via the idp_cert setting. This is highly recommended for production to prevent collision attacks.
    2. Fingerprint: Provide the fingerprint via idp_cert_fingerprint and specify the algorithm via idp_cert_fingerprint_algorithm (e.g., "http://www.w3.org/2000/09/xmldsig#sha1").

    Warning: Fingerprints are susceptible to collision attacks. For production environments, always use the full idp_cert.

  3. How the SAML Initialization and Consumption flow works

    master

    The SAML flow consists of two main phases: Initialization and Consumption.

    1. Initialization Phase: Your application receives a request to start authentication. You use OneLogin::RubySaml::Authrequest to create a SAML request and redirect the user to the Identity Provider (IdP).
    2. Consumption Phase: After the user authenticates at the IdP, the IdP redirects the user back to your application's Assertion Consumer Service (ACS) URL with a SAMLResponse. You use OneLogin::RubySaml::Response to validate this response and extract user information like nameid and attributes.

    Important Note on Encryption: If the SAMLResponse contains an encrypted assertion, you must provide the settings object during the initialization of OneLogin::RubySaml::Response so the library can use your Service Provider private key to decrypt it.

    # 1. Initialization
    def init
      request = OneLogin::RubySaml::Authrequest.new
      redirect_to(request.create(saml_settings))
    end
    
    # 2. Consumption
    def consume
      response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings)
      if response.is_valid?
         session[:userid] = response.nameid
         session[:attributes] = response.attributes
      else
        # handle error
      end
    end
  4. Install ruby-saml via Bundler or RubyGems

    master

    To use Ruby SAML, install the gem using Bundler (recommended) or RubyGems. You can pin to the latest stable version or track the master branch for bleeding-edge features.

    Using Gemfile (Bundler): To use the latest stable version:

    gem 'ruby-saml', '~> 1.18.0'

    To track the master branch:

    gem 'ruby-saml', :github => 'saml-toolkits/ruby-saml'

    Using RubyGems:

    gem install ruby-saml
  5. Ensure compatibility with other SAML implementations using raw_get_params (v1.6.0+)

    master

    When constructing Logoutresponse or SloLogoutrequest instances, avoid using options[:get_params] for _SAMLResponse_, _RelayState_, or _SigAlg_. Using decoded parameters can cause signature validation failures with other implementations (like Microsoft ADFS) due to differences in URI-encoding.

    Instead, use the options[:raw_get_params] parameter to provide the encoded parameters exactly as they were sent by the IdP.

    # Use raw_get_params to ensure signature compatibility with other SAML providers
    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?
  6. Handle clock drift in SAML responses

    master

    If you encounter the error Current time is earlier than NotBefore condition, it is likely due to clock desynchronization between your server and the Identity Provider (IdP).

    To mitigate this, you can pass the :allowed_clock_drift option when initializing a OneLogin::RubySaml::Response. This value (in seconds) is added to the current time before validating the NotBefore assertion. Keep this value as small as possible to maintain security.

    response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1.second)
  7. Implement Single Logout (SLO)

    master

    Ruby SAML supports both SP-initiated and IdP-initiated Single Logout.

    SP-Initiated SLO

    1. Create a OneLogin::RubySaml::Logoutrequest.
    2. Store the uuid in the session to match the incoming response.
    3. Redirect the user to the generated logout URL.
    4. Process the LogoutResponse from the IdP using OneLogin::RubySaml::Logoutresponse and validate it.

    IdP-Initiated SLO

    1. Catch the SAMLRequest from the IdP.
    2. Use OneLogin::RubySaml::SloLogoutrequest to validate the request.
    3. Generate a OneLogin::RubySaml::SloLogoutresponse to reply to the IdP.

    Note for ADFS: If using ADFS, you may need to set settings.security[:lowercase_url_encoding] = true for signature verification compatibility.

    # Example: SP-initiated Logout Request
    logout_request = OneLogin::RubySaml::Logoutrequest.new
    # ... setup session/transaction_id ...
    redirect_to(logout_request.create(settings, :RelayState => relay_state))
    
    # Example: Processing IdP-initiated Logout Request
    settings.security[:lowercase_url_encoding] = true
    logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: settings)
    
    if logout_request.is_valid?
      # ... delete session ...
      logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, :RelayState => params[:RelayState])
      redirect_to logout_response
    end
  8. Configure settings using IdP Metadata

    master

    Instead of manually specifying IdP attributes, you can use OneLogin::RubySaml::IdpMetadataParser to automatically populate OneLogin::RubySaml::Settings from an IdP's XML metadata file. This handles entity IDs, SSO/SLO service URLs, attribute names, and X.509 certificates.

    Use #parse_remote to fetch metadata from a URL. After parsing, you should still manually configure your Service Provider (SP) specific settings like assertion_consumer_service_url and sp_entity_id.

    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
  9. Validate Metadata Signature manually

    master

    The library does not currently provide a built-in method to validate the signature of the metadata XML itself. To do this securely, you must:

    1. Download the XML.
    2. Use a library like xml_security to validate the signature against a known certificate.
    3. Pass the validated XML string to IdpMetadataParser#parse.
    require "xml_security"
    require "onelogin/ruby-saml/utils"
    require "onelogin/ruby-saml/idp_metadata_parser"
    
    # ... (Setup Net::HTTP to fetch XML) ...
    
    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 Signature failed to be verified with the cert provided"
    end
  10. Prevent Replay Attacks using Assertion IDs

    master

    The library validates the NotBefore and NotOnOrAfter windows, but it does not prevent an attacker from replaying a valid assertion within that window. To defend against this, you must:

    1. Extract the ID: After successful validation, retrieve the ID via response.assertion_id.
    2. Store and Check: Store this ID in a persistent, shared cache (like Redis) with an expiration time matching the assertion's validity window (plus any allowed_clock_drift).
    3. Reject Duplicates: If the ID already exists in the cache, reject the request as a replay attack.
    # In your `consume` action, after a successful validation:
    if response.is_valid?
      assertion_id = response.assertion_id
      authorize_failure("Assertion ID is mandatory") if assertion_id.nil?
    
      assertion_not_on_or_after = response.not_on_or_after
      assertion_expiry = (Time.now.utc + 300) if assertion_not_on_or_after.nil?
    
      if is_new_assertion?(assertion_id, expires_at: assertion_expiry)
        session[:userid] = response.nameid
        session[:attributes] = response.attributes
      else
        authorize_failure("Replay attack detected")
      end
    else
      authorize_failure("Invalid response")
    end
    
    # Example Redis implementation for is_new_assertion?
    def is_new_assertion?(assertion_id, expires_at)
      ttl = (expires_at - Time.now.utc).to_i
      return false if ttl <= 0
    
      $redis.set("saml_assertion_ids:#{assertion_id}", "1", ex: ttl, nx: true)
    end
  11. Enforce SP-Initiated Flow with InResponseTo validation

    master

    To prevent IdP-initiated logins and ensure you only accept assertions for requests you actually made, implement InResponseTo validation:

    1. Store Request ID: When creating an OneLogin::RubySaml::Authrequest, store its uuid in the user's session before redirecting to the IdP.
    2. Validate Response: When the SAMLResponse arrives, retrieve the ID from the session (using session.delete to ensure it's single-use) and pass it to the OneLogin::RubySaml::Response constructor using the matches_request_id option.
    # 1. Store the AuthnRequest ID
    def init
      request = OneLogin::RubySaml::Authrequest.new
      session[:saml_request_id] = request.uuid
      redirect_to(request.create(saml_settings))
    end
    
    # 2. Validate the InResponseTo value
    def consume
      request_id = session.delete(:saml_request_id)
      raise "IdP-initiated detected" if request_id.nil?
    
      response = OneLogin::RubySaml::Response.new(
        params[:SAMLResponse],
        settings: saml_settings,
        matches_request_id: request_id
      )
    
      if response.is_valid?
        # ... authorize user
      end
    end