From a4709f9da000a98be13542e51185ddc5f882ba4b Mon Sep 17 00:00:00 2001 From: taitus Date: Mon, 11 Jul 2022 13:06:44 +0200 Subject: [PATCH 1/8] Add omniauth saml section for sign in and sign up page Co-authored-by: Anamika Aggarwal --- Gemfile | 1 + Gemfile.lock | 7 ++ app/assets/stylesheets/layout.scss | 14 ++- .../admin/settings/features_tab_component.rb | 1 + .../devise/omniauth_form_component.rb | 3 +- .../users/omniauth_callbacks_controller.rb | 6 + app/models/setting.rb | 1 + config/initializers/devise.rb | 4 + config/locales/en/general.yml | 4 + config/locales/en/settings.yml | 2 + config/locales/es/general.yml | 4 + config/locales/es/settings.yml | 2 + config/secrets.yml.example | 9 ++ .../devise/omniauth_form_component_spec.rb | 10 ++ spec/system/users_auth_spec.rb | 115 ++++++++++++++++++ 15 files changed, 181 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 158edf343..bd5fa3e03 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.1.0" 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..dd038b152 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.1.0) + omniauth (~> 2.0) + ruby-saml (~> 1.12) 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.15.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.1.0) 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/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..599d658cb 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -286,6 +286,10 @@ 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) } + config.omniauth :saml, + sp_entity_id: Rails.application.secrets.saml_sp_entity_id, + idp_cert: Rails.application.secrets.saml_idp_cert, + idp_sso_service_url: Rails.application.secrets.saml_idp_sso_service_url # ==> 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..9ed114348 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -91,6 +91,9 @@ staging: wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "" + saml_idp_cert: "" + saml_idp_sso_service_url: "" <<: *maps <<: *apis @@ -147,6 +150,9 @@ preproduction: wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "" + saml_idp_cert: "" + saml_idp_sso_service_url: "" <<: *maps <<: *apis @@ -202,5 +208,8 @@ production: wordpress_oauth2_key: "" wordpress_oauth2_secret: "" wordpress_oauth2_site: "" + saml_sp_entity_id: "" + saml_idp_cert: "" + saml_idp_sso_service_url: "" <<: *maps <<: *apis 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/system/users_auth_spec.rb b/spec/system/users_auth_spec.rb index 31cb9bd53..6d48fbdcb 100644 --- a/spec/system/users_auth_spec.rb +++ b/spec/system/users_auth_spec.rb @@ -488,6 +488,121 @@ 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 + end end scenario "Sign out" do From 6190d808ba51c7162aeb25a41e108158675e3762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Wed, 29 Jun 2022 20:55:32 +0200 Subject: [PATCH 2/8] Allow different times between IDP and SP machines We were having an issue because there was a difference of about 11 seconds between the local times of our machines and the time of the IDP server. Since right now we can't guarantee the time of these machines is fully synchronized, for now we're adding a margin of error of one minute. --- config/initializers/devise.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 599d658cb..2300379d0 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -289,7 +289,8 @@ Devise.setup do |config| config.omniauth :saml, sp_entity_id: Rails.application.secrets.saml_sp_entity_id, idp_cert: Rails.application.secrets.saml_idp_cert, - idp_sso_service_url: Rails.application.secrets.saml_idp_sso_service_url + idp_sso_service_url: Rails.application.secrets.saml_idp_sso_service_url, + allowed_clock_drift: 1.minute # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or From 5cb5f19e4fb6a169756afff0a89b8dc0ef5064de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sen=C3=A9n=20Rodero=20Rodr=C3=ADguez?= Date: Wed, 29 Jun 2022 11:36:11 +0200 Subject: [PATCH 3/8] Use IDPMetadataParse to set up the Idp SAML settings Co-authored-by: Anamika Aggarwal --- config/initializers/devise.rb | 11 ++++++----- config/secrets.yml.example | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 2300379d0..0a3fd7762 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -286,11 +286,12 @@ 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) } - config.omniauth :saml, - sp_entity_id: Rails.application.secrets.saml_sp_entity_id, - idp_cert: Rails.application.secrets.saml_idp_cert, - idp_sso_service_url: Rails.application.secrets.saml_idp_sso_service_url, - allowed_clock_drift: 1.minute + 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 + config.omniauth :saml, saml_settings # ==> 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 9ed114348..7f1c92e36 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -92,7 +92,7 @@ staging: wordpress_oauth2_secret: "" wordpress_oauth2_site: "" saml_sp_entity_id: "" - saml_idp_cert: "" + saml_idp_metadata_url: "" saml_idp_sso_service_url: "" <<: *maps <<: *apis @@ -151,7 +151,7 @@ preproduction: wordpress_oauth2_secret: "" wordpress_oauth2_site: "" saml_sp_entity_id: "" - saml_idp_cert: "" + saml_idp_metadata_url: "" saml_idp_sso_service_url: "" <<: *maps <<: *apis @@ -209,7 +209,7 @@ production: wordpress_oauth2_secret: "" wordpress_oauth2_site: "" saml_sp_entity_id: "" - saml_idp_cert: "" + saml_idp_metadata_url: "" saml_idp_sso_service_url: "" <<: *maps <<: *apis From fcbace069e51fdb1befb3c6844d63f196dc068b1 Mon Sep 17 00:00:00 2001 From: taitus Date: Wed, 12 Apr 2023 12:47:25 +0200 Subject: [PATCH 4/8] Only load saml settings when we have related secrets Without this change the IdpMetaParser would give an error in the Devise initializer when starting the application. I found it annoying to have to connect to the VPN so I decided to add this condition. Reviewer, feel free to consider this commit unnecessary and ask to revert it. --- config/initializers/devise.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 0a3fd7762..40b0cbba0 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -286,11 +286,14 @@ 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) } - 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 + 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 # ==> Warden configuration From b3737ae973c0bb57cbe171bacb566e9123c5c92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javi=20Mart=C3=ADn?= Date: Wed, 23 Jul 2025 12:44:43 +0200 Subject: [PATCH 5/8] Adjust spacing in OAuth documentation We're also fixing a missing "/" in one URL. --- docs/en/features/oauth.md | 18 +++++++++--------- docs/es/features/oauth.md | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/en/features/oauth.md b/docs/en/features/oauth.md index 934140298..af5a3617f 100644 --- a/docs/en/features/oauth.md +++ b/docs/en/features/oauth.md @@ -8,17 +8,17 @@ For each platform, go to their developers section and follow their guides to cre ## 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 ``` So for example the URL for Facebook application would be `yourdomain.com/users/auth/facebook/callback`. diff --git a/docs/es/features/oauth.md b/docs/es/features/oauth.md index 93c2523f8..1c6056dd5 100644 --- a/docs/es/features/oauth.md +++ b/docs/es/features/oauth.md @@ -8,17 +8,17 @@ Para cada plataforma, sigue las instrucciones en la sección de desarrolladores ## 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 ``` Por ejemplo para Facebook la URL sería `yourdomain.com/users/auth/facebook/callback`. From 26df59af02ad810d12e4ffabcc7ee254c43ea760 Mon Sep 17 00:00:00 2001 From: Anamika Aggarwal Date: Tue, 22 Jul 2025 16:30:19 +0000 Subject: [PATCH 6/8] Add SAML Integration doc to setup secrets file --- docs/en/SUMMARY.md | 2 +- docs/en/features/oauth.md | 11 ++++++++--- docs/es/SUMMARY.md | 2 +- docs/es/features/oauth.md | 11 ++++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) 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 af5a3617f..9aa6593e1 100644 --- a/docs/en/features/oauth.md +++ b/docs/en/features/oauth.md @@ -1,10 +1,10 @@ -# 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 @@ -19,6 +19,8 @@ user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:forma 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 1c6056dd5..62debfb90 100644 --- a/docs/es/features/oauth.md +++ b/docs/es/features/oauth.md @@ -1,10 +1,10 @@ -# 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 @@ -19,6 +19,8 @@ user_google_oauth2_omniauth_authorize GET|POST /users/auth/google_oauth2(.:forma 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" ``` From 5726bcef07da11921972a9f9c66c06e4227bbda5 Mon Sep 17 00:00:00 2001 From: Anamika Aggarwal Date: Fri, 13 Jun 2025 03:57:07 +0000 Subject: [PATCH 7/8] Update the omniauth-saml to 2.2.4 --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index bd5fa3e03..ae58b90ef 100644 --- a/Gemfile +++ b/Gemfile @@ -38,7 +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.1.0" +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 dd038b152..d4c6b3663 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -440,9 +440,9 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.1.0) - omniauth (~> 2.0) - ruby-saml (~> 1.12) + omniauth-saml (2.2.4) + omniauth (~> 2.1) + ruby-saml (~> 1.18) omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack @@ -612,7 +612,7 @@ GEM rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) - ruby-saml (1.15.0) + ruby-saml (1.18.0) nokogiri (>= 1.13.10) rexml ruby-vips (2.2.3) @@ -803,7 +803,7 @@ DEPENDENCIES omniauth-facebook (~> 10.0.0) omniauth-google-oauth2 (~> 1.2.1) omniauth-rails_csrf_protection (~> 1.0.2) - omniauth-saml (~> 2.1.0) + omniauth-saml (~> 2.2.4) omniauth-twitter (~> 1.4.0) paranoia (~> 3.0.1) pdf-reader (~> 2.14.1) From c9bf7797a09c444b5dcec25b302dec35f481b582 Mon Sep 17 00:00:00 2001 From: Anamika Aggarwal Date: Wed, 16 Jul 2025 07:49:37 +0000 Subject: [PATCH 8/8] 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