Integrate SAML in rails application

Viewed 851

I am trying to add the ruby-saml in my project. But I am a bit confused about how to implement it in my scenario. I am on a website let's say abc.com and there is a button. When I click on the button, I need to redirect to the website xyz.com where I need to pass SAML XML and send it to xyz.com/SAML. The SAML request will be processed by xyz.com and then they will send me a response. Could anyone give me some idea how to achieve it?

Also, I am confused about these fields. Could someone give me a quick summary?

settings.assertion_consumer_service_url
settings.sp_entity_id
settings.idp_entity_id
settings.idp_sso_service_url
settings.idp_slo_service_url

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



 def saml_settings
  settings = OneLogin::RubySaml::Settings.new

  settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
  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_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
1 Answers

Do you want to:

  • a.) enable SAML Login from another service, like Microsoft Azure Directory, OneLogin etc.? (then you are an SP = Service Provider)
  • b.) your app has users and offers a Login service for other apps (IDP = Identity Provider)

I suppose it's a)? Then it depends on: is it a provider per customer, so is it dynamically? Can each customer configure their own IDP, or is it one fixed for the whole app? If latter, then I would strongly suggest to use omniauth-saml instead, which is much easier to configure.

But if you want to use Enterprise Sign In Per Customer, then here is how we do it.

  • First: I never wrangled with all the specific settings and urls, Saml can autoconfigure itself via "metadata url", We let our customers specify a metadata-uri, that uri has all the information we need, so we can parse it with the supplied class:
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
settings = idp_metadata_parser.parse_remote(organisation.idp_meta_data_url)

We then add our own settings + route information to it:

   settings.assertion_consumer_service_url = "https://#{request.host}/saml/consume/#{organisation.id}"
        settings.issuer                         = "https://#{@request.host}/saml/metadata/#{organisation.id}"
        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"
# You would need a normal certificate/private key to enable signature stuff
        settings.certificate = File.read('config/saml/certificate.crt')
        settings.private_key = File.read('config/saml/private_key.key')
# In our case customer can optional activate signature validation:
        if organisation.signature_enabled?
          settings.security[:authn_requests_signed]   = true     # Enable or not signature on AuthNRequest
          settings.security[:logout_requests_signed]  = true     # Enable or not signature on Logout Request
          settings.security[:logout_responses_signed] = true     # Enable or not signature on Logout Response
          settings.security[:want_assertions_signed]  = true     # Enable or not the requirement of signed assertion
          settings.security[:metadata_signed]         = true     # Enable or not signature on Metadata

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

Request / Response

these are more or less from the ruby-saml examples:

Controller:

  skip_before_action :verify_authenticity_token

  def init
    @saml_request = OneLogin::RubySaml::Authrequest.new
    redirect_url = @saml_request.create(saml_settings)
    if redirect_uri
      redirect_to(redirect_uri)
    else
      @error = t('saml_controller.error')
      render 'error'
    end
  end

  def consume
    @saml_response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
    @saml_response.settings = saml_settings
  
    if @saml_response.is_valid?
      # do application logic, create user/update user sign in user
      sign_in(....) 
      session[:session_valid_for] = 12.hours.from_now.to_i
      redirect_to '/'
      else
        redirect_to '/watcher/profile'
      end
    else
      @error = @saml_response.errors
      render 'error'
    end
  end

Metadata

Most customers want a metadata-uri, too, to add the SP into their IDP (and configure that part automatically too), so you also need to provide a metadata, e.g. "/saml/#{org.id}/metadata" and return the metadata:

def metadata
  meta = OneLogin::RubySaml::Metadata.new
  render xml: meta.generate(saml_settings), content_type: "application/samlmetadata+xml"
end

Update: Using omniauth-saml

We also use Omniauth saml in an app. It is quite simple, but the config depends on the server you want to integrate with. You will need a sso target url (the consume url from the other side) and a certificate fingerprint or certificate for security. Also you can specify the "name Identifier", E-Mail or username or stuff like this.

Rails.application.config.middleware.use OmniAuth::Builder do
  use OmniAuth::Strategies::SAML,
    idp_sso_target_url: "??",
    idp_slo_target_url: "??",
    idp_cert_fingerprint: "??",
    name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", 
    # or "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" for E-Mail
    issuer: "YourAppName.com"

Have a look at omniauth-saml's good Readme doc for a list of all options. You can also request more attributes from OneLogin/IDP by using the request_attributes:

:request_attributes - Used to build the metadata file to inform the IdP to send certain attributes along with the SAMLResponse messages. Defaults to requesting name, first_name, last_name and email attributes. See the OneLogin::RubySaml::AttributeService class in the Ruby SAML gem for the available options for each attribute.

Update: Passing dynamic parameters to omniauth saml's redirect to Idp:

There seems to be no option in Omniauth-saml to have dynamic parameters, so you can try to patch/override the behavior. Omniauth-SAML is a Rack-middleware, so you only have access to the request object but not normal Rails stuff. If you trust your users (which you should not) then you can put the info in the param to omniauth: /auth/saml?something1=foo&bar=2, or you could encrypt the parameters with ActiveSupport Message Encryptor.

If you then know how to extract the dynamic parameters from the request you can apply this patch dynamically, because Ruby!

# put this into
#
# config/initializers/omniauth_patch.rb
#
module OmniauthPatch
  def additional_params_for_authn_request
    # here you should have access to the current request
    # try around with binding.irb what you can do
    binding.irb
    # return parameters you want to pass to the saml redirect
    {
      email: email,
      # ...
    }
  end
end

OmniAuth::Strategies::SAML.include OmniauthPatch
Related