From c9bf7797a09c444b5dcec25b302dec35f481b582 Mon Sep 17 00:00:00 2001 From: Anamika Aggarwal Date: Wed, 16 Jul 2025 07:49:37 +0000 Subject: [PATCH] Add multi-tenancy support for SAML --- app/lib/omniauth_tenant_setup.rb | 23 +++++++ config/initializers/devise.rb | 2 +- config/secrets.yml.example | 6 +- spec/lib/omniauth_tenant_setup_spec.rb | 87 ++++++++++++++++++++++++++ spec/system/users_auth_spec.rb | 42 +++++++++++++ 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 spec/lib/omniauth_tenant_setup_spec.rb diff --git a/app/lib/omniauth_tenant_setup.rb b/app/lib/omniauth_tenant_setup.rb index 1aef1cad4..ff93d681e 100644 --- a/app/lib/omniauth_tenant_setup.rb +++ b/app/lib/omniauth_tenant_setup.rb @@ -16,6 +16,11 @@ module OmniauthTenantSetup oauth2(env, secrets.wordpress_oauth2_key, secrets.wordpress_oauth2_secret) end + def saml(env) + saml_auth(env, secrets.saml_sp_entity_id, + secrets.saml_idp_metadata_url, secrets.saml_idp_sso_service_url) + end + private def oauth(env, key, secret) @@ -32,6 +37,24 @@ module OmniauthTenantSetup end end + def saml_auth(env, sp_entity_id, idp_metadata_url, idp_sso_service_url) + unless Tenant.default? + strategy = env["omniauth.strategy"] + + strategy.options[:sp_entity_id] = sp_entity_id if sp_entity_id.present? + strategy.options[:idp_metadata_url] = idp_metadata_url if idp_metadata_url.present? + strategy.options[:idp_sso_service_url] = idp_sso_service_url if idp_sso_service_url.present? + + if strategy.options[:issuer].present? && sp_entity_id.present? + strategy.options[:issuer] = sp_entity_id + end + + if strategy.options[:idp_metadata].present? && idp_metadata_url.present? + strategy.options[:idp_metadata] = idp_metadata_url + end + end + end + def secrets Tenant.current_secrets end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 40b0cbba0..88a32728a 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -294,7 +294,7 @@ Devise.setup do |config| saml_settings[:sp_entity_id] = Rails.application.secrets.saml_sp_entity_id saml_settings[:allowed_clock_drift] = 1.minute end - config.omniauth :saml, saml_settings + config.omniauth :saml, saml_settings.merge(setup: ->(env) { OmniauthTenantSetup.saml(env) }) # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/secrets.yml.example b/config/secrets.yml.example index 7f1c92e36..4048b8486 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -81,7 +81,7 @@ staging: # secret_key: my_secret_value # # Currently you can overwrite SMTP, SMS, manager, microsoft API, - # HTTP basic, twitter, facebook, google, wordpress and security settings. + # HTTP basic, twitter, facebook, google, wordpress, SAML and security settings. twitter_key: "" twitter_secret: "" facebook_key: "" @@ -140,7 +140,7 @@ preproduction: # secret_key: my_secret_value # # Currently you can overwrite SMTP, SMS, manager, microsoft API, - # HTTP basic, twitter, facebook, google, wordpress and security settings. + # HTTP basic, twitter, facebook, google, wordpress, SAML and security settings. twitter_key: "" twitter_secret: "" facebook_key: "" @@ -198,7 +198,7 @@ production: # secret_key: my_secret_value # # Currently you can overwrite SMTP, SMS, manager, microsoft API, - # HTTP basic, twitter, facebook, google, wordpress and security settings. + # HTTP basic, twitter, facebook, google, wordpress, SAML and security settings. twitter_key: "" twitter_secret: "" facebook_key: "" diff --git a/spec/lib/omniauth_tenant_setup_spec.rb b/spec/lib/omniauth_tenant_setup_spec.rb new file mode 100644 index 000000000..77a9fff54 --- /dev/null +++ b/spec/lib/omniauth_tenant_setup_spec.rb @@ -0,0 +1,87 @@ +require "rails_helper" + +describe OmniauthTenantSetup do + describe "#saml" do + it "uses different secrets for different tenants" do + create(:tenant, schema: "mars") + create(:tenant, schema: "venus") + + stub_secrets( + saml_sp_entity_id: "https://default.consul.dev/saml/metadata", + saml_idp_metadata_url: "https://default-idp.example.com/metadata", + saml_idp_sso_service_url: "https://default-idp.example.com/sso", + tenants: { + mars: { + saml_sp_entity_id: "https://mars.consul.dev/saml/metadata", + saml_idp_metadata_url: "https://mars-idp.example.com/metadata", + saml_idp_sso_service_url: "https://mars-idp.example.com/sso" + }, + venus: { + saml_sp_entity_id: "https://venus.consul.dev/saml/metadata", + saml_idp_metadata_url: "https://venus-idp.example.com/metadata", + saml_idp_sso_service_url: "https://venus-idp.example.com/sso" + } + } + ) + + Tenant.switch("mars") do + mars_env = { + "omniauth.strategy" => double(options: {}), + "HTTP_HOST" => "mars.consul.dev" + } + + OmniauthTenantSetup.saml(mars_env) + mars_strategy_options = mars_env["omniauth.strategy"].options + + expect(mars_strategy_options[:sp_entity_id]).to eq "https://mars.consul.dev/saml/metadata" + expect(mars_strategy_options[:idp_metadata_url]).to eq "https://mars-idp.example.com/metadata" + expect(mars_strategy_options[:idp_sso_service_url]).to eq "https://mars-idp.example.com/sso" + end + + Tenant.switch("venus") do + venus_env = { + "omniauth.strategy" => double(options: {}), + "HTTP_HOST" => "venus.consul.dev" + } + + OmniauthTenantSetup.saml(venus_env) + venus_strategy_options = venus_env["omniauth.strategy"].options + + expect(venus_strategy_options[:sp_entity_id]).to eq "https://venus.consul.dev/saml/metadata" + expect(venus_strategy_options[:idp_metadata_url]).to eq "https://venus-idp.example.com/metadata" + expect(venus_strategy_options[:idp_sso_service_url]).to eq "https://venus-idp.example.com/sso" + end + end + + it "uses default secrets for non-overridden tenant" do + create(:tenant, schema: "earth") + + stub_secrets( + saml_sp_entity_id: "https://default.consul.dev/saml/metadata", + saml_idp_metadata_url: "https://default-idp.example.com/metadata", + saml_idp_sso_service_url: "https://default-idp.example.com/sso", + tenants: { + mars: { + saml_sp_entity_id: "https://mars.consul.dev/saml/metadata", + saml_idp_metadata_url: "https://mars-idp.example.com/metadata", + saml_idp_sso_service_url: "https://mars-idp.example.com/sso" + } + } + ) + + Tenant.switch("earth") do + earth_env = { + "omniauth.strategy" => double(options: {}), + "HTTP_HOST" => "earth.consul.dev" + } + + OmniauthTenantSetup.saml(earth_env) + earth_strategy_options = earth_env["omniauth.strategy"].options + + expect(earth_strategy_options[:sp_entity_id]).to eq "https://default.consul.dev/saml/metadata" + expect(earth_strategy_options[:idp_metadata_url]).to eq "https://default-idp.example.com/metadata" + expect(earth_strategy_options[:idp_sso_service_url]).to eq "https://default-idp.example.com/sso" + end + end + end +end diff --git a/spec/system/users_auth_spec.rb b/spec/system/users_auth_spec.rb index 6d48fbdcb..61b4dbcf0 100644 --- a/spec/system/users_auth_spec.rb +++ b/spec/system/users_auth_spec.rb @@ -602,6 +602,48 @@ describe "Users" do expect(page).to have_field "Email", with: "tester@consul.dev" end + + scenario "SAML user from one tenant cannot sign in to another tenant", :seed_tenants do + %w[mars venus].each do |schema| + create(:tenant, schema: schema) + Tenant.switch(schema) { Setting["feature.saml_login"] = true } + end + + Tenant.switch("mars") do + mars_user = create(:user, username: "marsuser", email: "mars@consul.dev") + create(:identity, uid: "mars-saml-123", provider: "saml", user: mars_user) + end + + mars_saml_hash = { + provider: "saml", + uid: "mars-saml-123", + info: { + name: "marsuser", + email: "mars@consul.dev" + } + } + OmniAuth.config.add_mock(:saml, mars_saml_hash) + + with_subdomain("mars") do + visit new_user_session_path + click_button "Sign in with SAML" + + expect(page).to have_content "Successfully identified as Saml" + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "marsuser" + end + + with_subdomain("venus") do + visit new_user_session_path + click_button "Sign in with SAML" + + expect(page).to have_content "To continue, please click on the confirmation " \ + "link that we have sent you via email" + end + end end end