diff --git a/Gemfile b/Gemfile index 158edf343..ae58b90ef 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,7 @@ gem "omniauth", "~> 2.1.3" gem "omniauth-facebook", "~> 10.0.0" gem "omniauth-google-oauth2", "~> 1.2.1" gem "omniauth-rails_csrf_protection", "~> 1.0.2" +gem "omniauth-saml", "~> 2.2.4" gem "omniauth-twitter", "~> 1.4.0" gem "paranoia", "~> 3.0.1" gem "pg", "~> 1.5.9" diff --git a/Gemfile.lock b/Gemfile.lock index 2e97a9adc..d4c6b3663 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -440,6 +440,9 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) + omniauth-saml (2.2.4) + omniauth (~> 2.1) + ruby-saml (~> 1.18) omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack @@ -609,6 +612,9 @@ GEM rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) + ruby-saml (1.18.0) + nokogiri (>= 1.13.10) + rexml ruby-vips (2.2.3) ffi (~> 1.12) logger @@ -797,6 +803,7 @@ DEPENDENCIES omniauth-facebook (~> 10.0.0) omniauth-google-oauth2 (~> 1.2.1) omniauth-rails_csrf_protection (~> 1.0.2) + omniauth-saml (~> 2.2.4) omniauth-twitter (~> 1.4.0) paranoia (~> 3.0.1) pdf-reader (~> 2.14.1) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index b767d1be6..c6ff179c9 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -1392,7 +1392,8 @@ table { .button.button-twitter, .button.button-facebook, .button.button-google, -.button.button-wordpress { +.button.button-wordpress, +.button.button-saml { color: inherit; font-weight: bold; @@ -1442,6 +1443,17 @@ table { } } +.button.button-saml { + @include has-fa-icon(lock, solid); + background: #eafee9; + border-left: 3px solid #3b9857; + + &::before { + color: #3b9857; + } +} + + // 14. Verification // ---------------- diff --git a/app/components/admin/settings/features_tab_component.rb b/app/components/admin/settings/features_tab_component.rb index 2feae25cd..c038f4778 100644 --- a/app/components/admin/settings/features_tab_component.rb +++ b/app/components/admin/settings/features_tab_component.rb @@ -4,6 +4,7 @@ class Admin::Settings::FeaturesTabComponent < ApplicationComponent feature.featured_proposals feature.facebook_login feature.google_login + feature.saml_login feature.twitter_login feature.wordpress_login feature.signature_sheets diff --git a/app/components/devise/omniauth_form_component.rb b/app/components/devise/omniauth_form_component.rb index fbde17268..f8af4a353 100644 --- a/app/components/devise/omniauth_form_component.rb +++ b/app/components/devise/omniauth_form_component.rb @@ -16,7 +16,8 @@ class Devise::OmniauthFormComponent < ApplicationComponent (:twitter if feature?(:twitter_login)), (:facebook if feature?(:facebook_login)), (:google_oauth2 if feature?(:google_login)), - (:wordpress_oauth2 if feature?(:wordpress_login)) + (:wordpress_oauth2 if feature?(:wordpress_login)), + (:saml if feature?(:saml_login)) ].compact end end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 9f8116759..e753f6d36 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,4 +1,6 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token, only: :saml + def twitter sign_in_with :twitter_login, :twitter end @@ -15,6 +17,10 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController sign_in_with :wordpress_login, :wordpress_oauth2 end + def saml + sign_in_with :saml_login, :saml + end + def after_sign_in_path_for(resource) if resource.registering_with_oauth finish_signup_path 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/app/models/setting.rb b/app/models/setting.rb index 4847454ce..8db5ef8da 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -85,6 +85,7 @@ class Setting < ApplicationRecord "feature.remote_census": nil, "feature.valuation_comment_notification": true, "feature.graphql_api": true, + "feature.saml_login": false, "feature.sdg": true, "feature.machine_learning": false, "feature.remove_investments_supports": true, diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 9baa69960..88a32728a 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -286,6 +286,15 @@ Devise.setup do |config| Rails.application.secrets.wordpress_oauth2_secret, client_options: { site: Rails.application.secrets.wordpress_oauth2_site }, setup: ->(env) { OmniauthTenantSetup.wordpress_oauth2(env) } + saml_settings = {} + if Rails.application.secrets.saml_idp_metadata_url.present? + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + saml_settings = idp_metadata_parser.parse_remote_to_hash(Rails.application.secrets.saml_idp_metadata_url) + saml_settings[:idp_sso_service_url] = Rails.application.secrets.saml_idp_sso_service_url + 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.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/locales/en/general.yml b/config/locales/en/general.yml index 163091a05..0c0745e00 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -278,6 +278,10 @@ en: info: sign_in: "Sign in with:" sign_up: "Sign up with:" + saml: + sign_in: Sign in with SAML + sign_up: Sign up with SAML + name: SAML or_fill: "Or fill the following form:" proposals: create: diff --git a/config/locales/en/settings.yml b/config/locales/en/settings.yml index 6e36bdc0d..065daad63 100644 --- a/config/locales/en/settings.yml +++ b/config/locales/en/settings.yml @@ -91,6 +91,8 @@ en: google_login_description: "Allow users to sign up with their Google Account" wordpress_login: "Wordpress login" wordpress_login_description: "Allow users to sign up with their Wordpress Account" + saml_login: "SAML login" + saml_login_description: "Allow users to sign up with SAML" featured_proposals: "Featured proposals" featured_proposals_description: "Shows featured proposals on index proposals page" signature_sheets: "Signature sheets" diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 1b803c460..db8622d81 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -275,6 +275,10 @@ es: sign_in: Entra con Twitter sign_up: Regístrate con Twitter name: Twitter + saml: + sign_in: Entra con SAML + sign_up: Regístrate con SAML + name: SAML info: sign_in: "Entra con:" sign_up: "Regístrate con:" diff --git a/config/locales/es/settings.yml b/config/locales/es/settings.yml index 154c26dec..28f73ca30 100644 --- a/config/locales/es/settings.yml +++ b/config/locales/es/settings.yml @@ -91,6 +91,8 @@ es: google_login_description: "Permitir que los usuarios se registren con su cuenta de Google" wordpress_login: "Registro con Wordpress" wordpress_login_description: "Permitir que los usuarios se registren con su cuenta de Wordpress" + saml_login: "Registro con SAML" + saml_login_description: "Permitir que los usuarios se registren usando SAML" featured_proposals: "Propuestas destacadas" featured_proposals_description: "Muestra propuestas destacadas en la página principal de propuestas" signature_sheets: "Hojas de firmas" diff --git a/config/secrets.yml.example b/config/secrets.yml.example index fdc9285a6..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: "" @@ -91,6 +91,9 @@ staging: wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "" + saml_idp_metadata_url: "" + saml_idp_sso_service_url: "" <<: *maps <<: *apis @@ -137,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: "" @@ -147,6 +150,9 @@ preproduction: wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "" + saml_idp_metadata_url: "" + saml_idp_sso_service_url: "" <<: *maps <<: *apis @@ -192,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: "" @@ -202,5 +208,8 @@ production: wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "" + saml_idp_metadata_url: "" + saml_idp_sso_service_url: "" <<: *maps <<: *apis diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 93217e4ab..244fcf54c 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -44,7 +44,7 @@ * [Tests](customization/tests.md) * [Technical Features](features/features.md) - * [OAuth](features/oauth.md) + * [Authentication with external services (OAuth)](features/oauth.md) * [GraphQL](features/graphql.md) * [Debates and proposals recommendations](features/recommendations.md) * [Configure Census Connection](features/census_configuration.md) diff --git a/docs/en/features/oauth.md b/docs/en/features/oauth.md index 934140298..9aa6593e1 100644 --- a/docs/en/features/oauth.md +++ b/docs/en/features/oauth.md @@ -1,24 +1,26 @@ -# OAuth +# Authentication with external services (OAuth) -You can configure authentication services with external OAuth providers, right now Twitter, Facebook, Google and Wordpress are supported. +You can configure authentication services with external OAuth providers. Right now, Twitter, Facebook, Google, Wordpress and SAML are supported. ## 1. Create an App on the platform -For each platform, go to their developers section and follow their guides to create an app. +For Twitter, Facebook, Google and Wordpress, go to their developers section and follow their guides to create an app. For SAML, you'll have to configure an Identity Provider (IdP). ## 2. Set the authentication URL of your Consul Democracy installation -They'll ask you for the authentication URL of your Consul Democracy installation, and as you can see running `rails routes |grep omniauth` at your Consul Democracy repo locally: +They'll ask you for the authentication URL of your Consul Democracy installation, and as you can see running `rails routes | grep omniauth` at your Consul Democracy repo locally: ```bash - user_twitter_omniauth_authorize GET|POST /users/auth/twitter(.:format) users/omniauth_callbacks#passthru - user_twitter_omniauth_callback GET|POST /users/auth/twitter/callback(.:format) users/omniauth_callbacks#twitter - user_facebook_omniauth_authorize GET|POST /users/auth/facebook(.:format) users/omniauth_callbacks#passthru - user_facebook_omniauth_callback GET|POST /users/auth/facebook/callback(.:format) users/omniauth_callbacks#facebook - user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) users omniauth_callbacks#passthru - user_google_oauth2_omniauth_callback GET|POST /users/auth/google_oauth2/callback(.:format) users/omniauth_callbacks#google_oauth2 - user_wordpress_oauth2_omniauth_authorize GET|POST /users/auth/wordpress_oauth2(.:format) users/omniauth_callbacks#passthru - user_wordpress_oauth2_omniauth_callback GET|POST /users/auth/wordpress_oauth2/callback(.:format) users/omniauth_callbacks#wordpress_oauth2 +user_twitter_omniauth_authorize GET|POST /users/auth/twitter(.:format) users/omniauth_callbacks#passthru +user_twitter_omniauth_callback GET|POST /users/auth/twitter/callback(.:format) users/omniauth_callbacks#twitter +user_facebook_omniauth_authorize GET|POST /users/auth/facebook(.:format) users/omniauth_callbacks#passthru +user_facebook_omniauth_callback GET|POST /users/auth/facebook/callback(.:format) users/omniauth_callbacks#facebook +user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) users/omniauth_callbacks#passthru +user_google_oauth2_omniauth_callback GET|POST /users/auth/google_oauth2/callback(.:format) users/omniauth_callbacks#google_oauth2 +user_wordpress_oauth2_omniauth_authorize GET|POST /users/auth/wordpress_oauth2(.:format) users/omniauth_callbacks#passthru +user_wordpress_oauth2_omniauth_callback GET|POST /users/auth/wordpress_oauth2/callback(.:format) users/omniauth_callbacks#wordpress_oauth2 +user_saml_omniauth_authorize GET|POST /users/auth/saml(.:format) users/omniauth_callbacks#passthru +user_saml_omniauth_callback GET|POST /users/auth/saml/callback(.:format) users/omniauth_callbacks#saml ``` So for example the URL for Facebook application would be `yourdomain.com/users/auth/facebook/callback`. @@ -37,4 +39,7 @@ When you complete the application registration you'll get a *key* and *secret* v wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "https://yoursp.org/entityid" + saml_idp_metadata_url: "https://youridp.org/api/saml/metadata" + saml_idp_sso_service_url: "https://youridp.org/api/saml/sso" ``` diff --git a/docs/es/SUMMARY.md b/docs/es/SUMMARY.md index 16eb41c87..18ef06383 100644 --- a/docs/es/SUMMARY.md +++ b/docs/es/SUMMARY.md @@ -44,7 +44,7 @@ * [Tests](customization/tests.md) * [Funcionalidades Técnicas](features/features.md) - * [OAuth](features/oauth.md) + * [Autenticación con servicios externos (OAuth)](features/oauth.md) * [GraphQL](features/graphql.md) * [Recomendaciones de debates y propuestas](features/recommendations.md) * [Configurar conexión con el Censo](features/census_configuration.md) diff --git a/docs/es/features/oauth.md b/docs/es/features/oauth.md index 93c2523f8..62debfb90 100644 --- a/docs/es/features/oauth.md +++ b/docs/es/features/oauth.md @@ -1,24 +1,26 @@ -# OAuth +# Autenticación con servicios externos (OAuth) -Puedes configurar la autenticación con servicios externos usando OAuth, actualmente se pueden utilizar Twitter, Facebook, Google y Wordpress. +Puedes configurar la autenticación con servicios externos usando OAuth. Actualmente, se pueden utilizar Twitter, Facebook, Google, Wordpress y SAML. ## 1. Crea una aplicación en la plataforma -Para cada plataforma, sigue las instrucciones en la sección de desarrolladores de su página web. +Para Twitter, Facebook, Google y Wordpress, sigue las instrucciones en la sección de desarrolladores de su página web. Para SAML, tendrás que configurar tu propio proveedor de identidad (IdP). ## 2. Establece la URL de autenticación de tu instalación de Consul Democracy -Te preguntarán por la URL de autenticación de tu instalación de Consul Democracy, y como podrás comprobar corriendo la tarea `rails routes |grep omniauth` en tu repositorio local: +Te preguntarán por la URL de autenticación de tu instalación de Consul Democracy, y como podrás comprobar corriendo la tarea `rails routes | grep omniauth` en tu repositorio local: ```bash - user_twitter_omniauth_authorize GET|POST /users/auth/twitter(.:format) users/omniauth_callbacks#passthru - user_twitter_omniauth_callback GET|POST /users/auth/twitter/callback(.:format) users/omniauth_callbacks#twitter - user_facebook_omniauth_authorize GET|POST /users/auth/facebook(.:format) users/omniauth_callbacks#passthru - user_facebook_omniauth_callback GET|POST /users/auth/facebook/callback(.:format) users/omniauth_callbacks#facebook - user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) users omniauth_callbacks#passthru - user_google_oauth2_omniauth_callback GET|POST /users/auth/google_oauth2/callback(.:format) users/omniauth_callbacks#google_oauth2 - user_wordpress_oauth2_omniauth_authorize GET|POST /users/auth/wordpress_oauth2(.:format) users/omniauth_callbacks#passthru - user_wordpress_oauth2_omniauth_callback GET|POST /users/auth/wordpress_oauth2/callback(.:format) users/omniauth_callbacks#wordpress_oauth2 +user_twitter_omniauth_authorize GET|POST /users/auth/twitter(.:format) users/omniauth_callbacks#passthru +user_twitter_omniauth_callback GET|POST /users/auth/twitter/callback(.:format) users/omniauth_callbacks#twitter +user_facebook_omniauth_authorize GET|POST /users/auth/facebook(.:format) users/omniauth_callbacks#passthru +user_facebook_omniauth_callback GET|POST /users/auth/facebook/callback(.:format) users/omniauth_callbacks#facebook +user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:format) users/omniauth_callbacks#passthru +user_google_oauth2_omniauth_callback GET|POST /users/auth/google_oauth2/callback(.:format) users/omniauth_callbacks#google_oauth2 +user_wordpress_oauth2_omniauth_authorize GET|POST /users/auth/wordpress_oauth2(.:format) users/omniauth_callbacks#passthru +user_wordpress_oauth2_omniauth_callback GET|POST /users/auth/wordpress_oauth2/callback(.:format) users/omniauth_callbacks#wordpress_oauth2 +user_saml_omniauth_authorize GET|POST /users/auth/saml(.:format) users/omniauth_callbacks#passthru +user_saml_omniauth_callback GET|POST /users/auth/saml/callback(.:format) users/omniauth_callbacks#saml ``` Por ejemplo para Facebook la URL sería `yourdomain.com/users/auth/facebook/callback`. @@ -37,4 +39,7 @@ Cuando completes el registro de la aplicación en su plataforma te darán un *ke wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "https://tusp.org/entityid" + saml_idp_metadata_url: "https://tuidp.org/api/saml/metadata" + saml_idp_sso_service_url: "https://tuidp.org/api/saml/sso" ``` diff --git a/spec/components/devise/omniauth_form_component_spec.rb b/spec/components/devise/omniauth_form_component_spec.rb index b50009776..f4e731206 100644 --- a/spec/components/devise/omniauth_form_component_spec.rb +++ b/spec/components/devise/omniauth_form_component_spec.rb @@ -9,6 +9,7 @@ describe Devise::OmniauthFormComponent do Setting["feature.twitter_login"] = false Setting["feature.google_login"] = false Setting["feature.wordpress_login"] = false + Setting["feature.saml_login"] = false end it "is not rendered when all authentications are disabled" do @@ -52,5 +53,14 @@ describe Devise::OmniauthFormComponent do expect(page).to have_button "Wordpress" expect(page).to have_button count: 1 end + + it "renders the SAML link when the feature is enabled" do + Setting["feature.saml_login"] = true + + render_inline component + + expect(page).to have_button "SAML" + expect(page).to have_button count: 1 + end end end 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 31cb9bd53..61b4dbcf0 100644 --- a/spec/system/users_auth_spec.rb +++ b/spec/system/users_auth_spec.rb @@ -488,6 +488,163 @@ describe "Users" do expect(page).to have_field "Email", with: "manuela@consul.dev" end end + + context "Saml" do + before { Setting["feature.saml_login"] = true } + + let(:saml_hash_with_email) do + { + provider: "saml", + uid: "ext-tester", + info: { + name: "samltester", + email: "tester@consul.dev" + } + } + end + + let(:saml_hash_with_verified_email) do + { + provider: "saml", + uid: "ext-tester", + info: { + name: "samltester", + email: "tester@consul.dev", + verified: "1" + } + } + end + + scenario "Sign up with a confirmed email" do + OmniAuth.config.add_mock(:saml, saml_hash_with_verified_email) + + visit new_user_registration_path + click_button "Sign up with SAML" + + expect(page).to have_content "Successfully identified as Saml" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "samltester" + + click_link "Change my login details" + + expect(page).to have_field "Email", with: "tester@consul.dev" + end + + scenario "Sign up with an unconfirmed email" do + OmniAuth.config.add_mock(:saml, saml_hash_with_email) + + visit new_user_registration_path + click_button "Sign up with SAML" + + expect(page).to have_content "To continue, please click on the confirmation " \ + "link that we have sent you via email" + + confirm_email + expect(page).to have_content "Your account has been confirmed" + expect(page).to have_current_path new_user_session_path + + click_button "Sign in with SAML" + + expect(page).to have_content "Successfully identified as Saml" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "samltester" + + click_link "Change my login details" + + expect(page).to have_field "Email", with: "tester@consul.dev" + end + + scenario "Sign in with a user with a SAML identity" do + user = create(:user, username: "samltester", email: "tester@consul.dev", password: "My123456") + create(:identity, uid: "ext-tester", provider: "saml", user: user) + OmniAuth.config.add_mock(:saml, { provider: "saml", uid: "ext-tester" }) + + visit new_user_session_path + click_button "Sign in with SAML" + + expect(page).to have_content "Successfully identified as Saml" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "samltester" + + click_link "Change my login details" + + expect(page).to have_field "Email", with: "tester@consul.dev" + end + + scenario "Sign in with a user without a SAML identity keeps the username" do + create(:user, username: "tester", email: "tester@consul.dev", password: "My123456") + OmniAuth.config.add_mock(:saml, saml_hash_with_verified_email) + + visit new_user_session_path + click_button "Sign in with SAML" + + expect(page).to have_content "Successfully identified as Saml" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "tester" + + click_link "Change my login details" + + 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 scenario "Sign out" do