omniauth / omniauth-okta

OAuth2 strategy for Okta
MIT License
41 stars 37 forks source link

Support for multi-tenancy ? #28

Closed almathie closed 2 years ago

almathie commented 2 years ago

Okta is quite explicit about their view on multi-tenancy (See: https://developer.okta.com/docs/guides/build-sso-integration/openidconnect/main/#multi-tenancy). They mandate full support of multi-tenancy to be added in the approved list of integration (the OIN network).

A typical SAAS service that serves B2B customers - who might be using Okta as their identity provider - will have to manage many client_id, secret, and site. Typically, one per customer using Okta, or even more than one according to the doc above.

As far as I can see, the config needs to be set in an initialiser, making it impossible to support multi-tenancy with this library. Is there something I am missing in this strategy that would allow for the config to be dynamically loaded on a customer-per-customer basis ?

rvracaric commented 2 years ago

Haven't tried it myself, but noticed this solution in one of the closed issues: https://github.com/omniauth/omniauth-okta/issues/19#issuecomment-851922139

almathie commented 2 years ago

Indeed I have been able to solve this using a setup_proc

Code looks like this :

 require "omniauth-okta"
  # Dynamically load the correct Okta provider configuration based on the request data contained in the request
  # This proc is executed by Omniauth only on routes registered by the okta strategy middleware
  # See: https://github.com/omniauth/omniauth/wiki/Setup-Phase
  okta_setup_proc = proc do |env|
    request = Rack::Request.new(env)

    # If the proc is executed during the request phase -> Look for tenant name in a `tenant` params
    # If the proc is executed during the callback phase ->  Extract tenant name from the `state` params
    tenant = if request.params["tenant"]
      tenant = request.params["tenant"]
    elsif request.params["state"]
      tenant = request.params["state"].split("|").first # See state setting below
    end
    tenant = Rails.application.config_for("sso")[:okta_providers].find { |provider| provider[:name] == tenant }
    return [404, { "Content-Type" => "text/html" }, ["Okta tenant not found"]] unless tenant

    # Override omniauth strategy config by tenant config
    env["omniauth.strategy"].options[:client_id] = tenant[:client_id]
    env["omniauth.strategy"].options[:client_secret] = tenant[:client_secret]
    env["omniauth.strategy"].options[:client_options] = {
      site:          tenant[:site],
      authorize_url: tenant[:authorize_url] || "#{tenant[:site]}/oauth2/v1/authorize",
      token_url:     tenant[:token_url]     || "#{tenant[:site]}/oauth2/v1/token",
      user_info_url: tenant[:user_info_url] || "#{tenant[:site]}/oauth2/v1/userinfo"
    }
    env["omniauth.strategy"].options[:state] = "#{tenant[:name]}|#{SecureRandom.hex(16)}"
  end
  # Load the okta strategy middleware
  config.omniauth(:okta, nil, nil, scope: "openid profile email",
    fields: %w[profile email],
    strategy_class: OmniAuth::Strategies::Okta,
    setup: okta_setup_proc)