Devise SAML Authenticatable

repository·master·Indexed 43 days ago

https://github.com/apokalipto/devise_saml_authenticatable

A Single-Sign-On (SSO) authentication strategy for the Devise gem that leverages the ruby-saml library to handle SAML-based authentication in Ruby on Rails applications.

Tokens
8.1K
Snippets
25
Records
33
Agent score
50%

What's inside Devise SAML Authenticatable

  1. Identify required IdP endpoints and metadata

    master

    When setting up your Identity Provider (IdP), you will need to provide the following endpoints for your application:

    • Assertion Consumer Service (ACS) / Login Endpoint: /users/saml/auth (where the IdP sends the SAML response).
    • Metadata / Audience: /users/saml/metadata.
    • Single Logout (SLO): /users/saml/idp_sign_out (if you want to support signing the user out of the application when they sign out of the IdP).

    You will also need to collect the following from your IdP to configure ruby-saml:

    • Issuer (idp_entity_id)
    • SSO endpoint (idp_sso_service_url)
    • SLO endpoint (idp_slo_service_url)
    • Certificate fingerprint (idp_cert_fingerprint) and algorithm (idp_cert_fingerprint_algorithm), or the certificate itself (idp_cert).
  2. Implement a custom IdP Settings Adapter

    master

    To support multiple Identity Providers (IdPs) or dynamic application domains, you can implement a custom settings adapter. This allows you to return specific SAML settings based on the idp_entity_id and the current request object.

    Implementation Steps:

    1. Create an adapter class with a class method self.settings(idp_entity_id, request).
    2. The settings method must return a hash of configuration settings for the corresponding IdP.
    3. Register the adapter in config/initializers/devise.rb using config.idp_settings_adapter = YourAdapterClass.

    Settings returned by the adapter will override the default settings defined in config/initializers/devise.rb.

    Entity ID Detection

    By default, the gem uses config.idp_entity_id_reader to find the Issuer in the SAML request. You can implement a custom reader by defining a class with an .entity_id method. Note that if you use encrypted assertions, your custom reader must be able to decrypt the response to identify the entity ID.

    class IdPSettingsAdapter
      def self.settings(idp_entity_id, request)
        case idp_entity_id
        when "http://www.example_idp_entity_id.com"
          {
            assertion_consumer_service_url: "#{request.protocol}#{request.host_with_port}/users/saml/auth",
            assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
            name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
            sp_entity_id: "#{request.protocol}#{request.host_with_port}/saml/metadata",
            idp_entity_id: "http://www.example_idp_entity_id.com",
            authn_context: "",
            idp_slo_service_url: "http://example_idp_slo_service_url.com",
            idp_sso_service_url: "http://example_idp_sso_service_url.com",
            idp_cert: "example_idp_cert"
          }
        else
          {}
        end
      end
    end
  3. Sign and Encrypt SAML Requests and Assertions

    master

    This gem utilizes ruby-saml for signing and decryption. To enable these features, you must provide the necessary public certificate and private key in your shared settings within config/initializers/devise.rb.

    If you are using multiple IdPs via an adapter, ensure the certificate and private key are available in the settings returned by your adapter or defined in the global configuration.

  4. Handle IdP Logout Requests

    master

    The gem supports Single Logout (SLO) requests initiated by the Identity Provider.

    To handle these requests, direct the IdP to the users/saml/idp_sign_out endpoint. This will invalidate the user's current sessions and log them out.

    To disable this feature, set saml_session_index_key to nil in your Devise configuration.

  5. Map SAML attributes to User attributes

    master

    You can map SAML attributes (often in URN or OID format) to your model's fields using one of two methods:

    1. Using a YAML configuration file

    Create config/attribute-map.yml to define a static mapping:

    "urn:mace:dir:attribute-def:uid": "user_name"
    "urn:mace:dir:attribute-def:email": "email"

    2. Using an Attribute Map Resolver

    For dynamic mapping (e.g., different mappings per IdP), implement a class inheriting from DeviseSamlAuthenticatable::DefaultAttributeMapResolver and assign it to config.saml_attribute_map_resolver in config/initializers/devise.rb.

    # app/lib/my_attribute_map_resolver.rb
    class MyAttributeMapResolver < DeviseSamlAuthenticatable::DefaultAttributeMapResolver
      def attribute_map
        issuer = saml_response.issuers.first
        case issuer
        when "idp_entity_id"
          {
            "urn:mace:dir:attribute-def:uid" => "user_name",
            "urn:mace:dir:attribute-def:email" => "email"
          }
        end
      end
    end
    
    # config/initializers/devise.rb
    Devise.setup do |config|
      config.saml_attribute_map_resolver = "MyAttributeMapResolver"
    end
  6. Configure user creation and attribute updates

    master

    In config/initializers/devise.rb, you can control whether users are automatically created or updated upon successful SAML authentication.

    • config.saml_create_user: Set to true to create a new user if they don't exist. Can accept a Proc to determine which model class to use.
    • config.saml_update_user: Set to true to update user attributes after login. Can accept a Proc for custom logic.
    • config.saml_update_resource_hook: A lambda called if creation or update is enabled. It receives the user, saml_response, and auth_value. Use this to map SAML attributes to model fields.
    • config.saml_resource_locator: A lambda used to resolve the saml_response and auth_value into the correct user object (e.g., finding a user by email).
    Devise.setup do |config|
      # Create user if the user does not exist. (Default is false)
      config.saml_create_user = true
    
      # Update the attributes of the user after a successful login. (Default is false)
      config.saml_update_user = true
    
      # Lambda that is called to resolve the saml_response and auth_value into the correct user object.
      config.saml_resource_locator = -> (model, saml_response, auth_value) {
        model.find_by(Devise.saml_default_user_key => auth_value)
      }
    end
  7. Use an IdP settings adapter for dynamic configuration

    master

    If you need to fetch SAML settings dynamically (e.g., from a database per tenant), you can configure an idp_settings_adapter on the Devise module.

    The adapter must implement a settings method. The gem supports two signatures for this method:

    1. settings(idp_entity_id)
    2. settings(idp_entity_id, request)

    If the adapter's settings method accepts one parameter, the gem passes only the idp_entity_id. If it accepts more, the gem passes both the idp_entity_id and the current request object. The returned object's attributes are then merged into the base Devise.saml_config.

  8. Configure :saml_authenticatable in Devise initializer

    master

    Use config.saml_configure within the Devise.setup block in config/initializers/devise.rb to pass settings directly to ruby-saml. This includes URLs, entity IDs, and certificate information.

    Devise.setup do |config|
      # ...
      config.saml_configure do |settings|
        settings.assertion_consumer_service_url     = "http://localhost:3000/users/saml/auth"
        settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        settings.name_identifier_format             = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
        settings.sp_entity_id                       = "http://localhost:3000/saml/metadata"
        settings.authn_context                      = ""
        settings.idp_slo_service_url                = "http://localhost/simplesaml/www/saml2/idp/SingleLogoutService.php"
        settings.idp_sso_service_url                = "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
        settings.idp_cert_fingerprint               = "00:A1:2B:3C:44:55:6F:A7:88:CC:DD:EE:22:33:44:55:D6:77:8F:99"
        settings.idp_cert_fingerprint_algorithm     = "http://www.w3.org/2000/09/xmldsig#sha1"
      end
    end
  9. Configure SAML settings via file-based or adapter-based methods

    master

    The saml_config method determines how Identity Provider (IdP) settings are loaded. It follows a specific precedence order:

    1. File-based configuration: If a file exists at #{Rails.root}/config/idp.yml, the gem loads settings from that YAML file using the current Rails.env as the top-level key. The file content should be compatible with OneLogin::RubySaml::Settings.
    2. Adapter-based configuration: If Devise.idp_settings_adapter is defined, the gem uses an adapter to fetch settings. The adapter's settings method is called, potentially passing idp_entity_id and/or the request object.
    3. Devise global configuration: If neither of the above is present, it falls back to Devise.saml_config.

    To use an adapter, ensure Devise.idp_settings_adapter is set to a class or object that responds to settings.

    # Example of file-based config structure in config/idp.yml
    production:
      idp_sso_target_url: "https://example.com/saml/sso"
      idp_cert: "..."