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