diff --git a/Gemfile b/Gemfile index ca3705f83..c44749eca 100644 --- a/Gemfile +++ b/Gemfile @@ -40,6 +40,7 @@ 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 "omniauth_openid_connect", "~> 0.8.0" gem "paranoia", "~> 3.0.1" gem "pg", "~> 1.5.9" gem "pg_search", "~> 2.3.7" diff --git a/Gemfile.lock b/Gemfile.lock index c0af5dcb8..515302daa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,6 +85,7 @@ GEM acts_as_votable (0.14.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + aes_key_wrap (1.1.0) afm (0.2.2) ahoy_matey (5.4.0) activesupport (>= 7.1) @@ -103,6 +104,7 @@ GEM ancestry (4.3.3) activerecord (>= 5.2.6) ast (2.4.3) + attr_required (1.0.2) audited (5.8.0) activerecord (>= 5.2, < 8.2) activesupport (>= 5.2, < 8.2) @@ -119,6 +121,7 @@ GEM parser (>= 2.4) smart_properties bigdecimal (3.2.2) + bindata (2.5.1) bindex (0.8.1) bing_translator (6.2.0) json @@ -215,6 +218,8 @@ GEM htmlentities (~> 4.3.3) launchy (>= 2.1, < 4.0) mail (~> 2.7) + email_validator (2.2.4) + activemodel erb_lint (0.9.0) activesupport better_html (>= 2.0.1) @@ -243,6 +248,8 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) faraday-net_http (3.4.0) net-http (>= 0.5.0) faraday-retry (2.3.1) @@ -317,6 +324,13 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.12.2) + json-jwt (1.16.7) + activesupport (>= 4.2) + aes_key_wrap + base64 + bindata + faraday (~> 2.0) + faraday-follow_redirects jwt (2.10.1) base64 kaminari (1.2.2) @@ -446,6 +460,22 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack + omniauth_openid_connect (0.8.0) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.3.1) + activemodel + attr_required (>= 1.0.0) + email_validator + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + mail + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_url + webfinger (~> 2.0) orm_adapter (0.5.0) ostruct (0.6.2) parallel (1.27.0) @@ -494,6 +524,13 @@ GEM rack (2.2.17) rack-accept (0.4.5) rack (>= 0.4) + rack-oauth2 (2.2.1) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) @@ -687,6 +724,11 @@ GEM net-ssh (>= 2.8.0) ostruct stringio (3.1.1) + swd (2.0.3) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) terrapin (0.6.0) @@ -713,6 +755,9 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.3) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix version_gem (1.1.8) view_component (3.23.2) activesupport (>= 5.2.0, < 8.1) @@ -729,6 +774,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webfinger (2.1.3) + activesupport + faraday (~> 2.0) + faraday-follow_redirects webrick (1.8.2) websocket (1.2.11) websocket-driver (0.7.7) @@ -805,6 +854,7 @@ DEPENDENCIES omniauth-rails_csrf_protection (~> 1.0.2) omniauth-saml (~> 2.2.4) omniauth-twitter (~> 1.4.0) + omniauth_openid_connect (~> 0.8.0) paranoia (~> 3.0.1) pdf-reader (~> 2.14.1) pg (~> 1.5.9) diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 0c69d0bae..d95e72179 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -1384,7 +1384,8 @@ table { .button.button-facebook, .button.button-google, .button.button-wordpress, -.button.button-saml { +.button.button-saml, +.button.button-oidc { color: inherit; font-weight: bold; @@ -1444,6 +1445,16 @@ table { } } +.button.button-oidc { + @include has-fa-icon(openid, brands); + background: #fdf9f1; + border-left: 3px solid #f7931e; + + &::before { + color: #f7931e; + } +} + // 14. Verification // ---------------- diff --git a/app/components/admin/settings/features_tab_component.rb b/app/components/admin/settings/features_tab_component.rb index c038f4778..67d54d337 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.oidc_login feature.saml_login feature.twitter_login feature.wordpress_login diff --git a/app/components/devise/omniauth_form_component.rb b/app/components/devise/omniauth_form_component.rb index f8af4a353..795ec24c8 100644 --- a/app/components/devise/omniauth_form_component.rb +++ b/app/components/devise/omniauth_form_component.rb @@ -17,7 +17,8 @@ class Devise::OmniauthFormComponent < ApplicationComponent (:facebook if feature?(:facebook_login)), (:google_oauth2 if feature?(:google_login)), (:wordpress_oauth2 if feature?(:wordpress_login)), - (:saml if feature?(:saml_login)) + (:saml if feature?(:saml_login)), + (:oidc if feature?(:oidc_login)) ].compact end end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index e753f6d36..b2976acd6 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -21,6 +21,10 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController sign_in_with :saml_login, :saml end + def oidc + sign_in_with :oidc_login, :oidc + 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 ff93d681e..c0909b0cd 100644 --- a/app/lib/omniauth_tenant_setup.rb +++ b/app/lib/omniauth_tenant_setup.rb @@ -21,6 +21,11 @@ module OmniauthTenantSetup secrets.saml_idp_metadata_url, secrets.saml_idp_sso_service_url) end + def oidc(env) + oidc_auth(env, secrets.oidc_client_id, + secrets.oidc_client_secret, secrets.oidc_issuer, secrets.oidc_redirect_uri) + end + private def oauth(env, key, secret) @@ -55,6 +60,17 @@ module OmniauthTenantSetup end end + def oidc_auth(env, client_id, client_secret, issuer, redirect_uri) + unless Tenant.default? + strategy = env["omniauth.strategy"] + + strategy.options[:client_id] = client_id if client_id.present? + strategy.options[:client_secret] = client_secret if client_secret.present? + strategy.options[:issuer] = issuer if issuer.present? + strategy.options[:redirect_uri] = redirect_uri if redirect_uri.present? + end + end + def secrets Tenant.current_secrets end diff --git a/app/models/setting.rb b/app/models/setting.rb index 8db5ef8da..d9b6f2fb1 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.oidc_login": false, "feature.saml_login": false, "feature.sdg": true, "feature.machine_learning": false, diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 88a32728a..44ff33b03 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -296,6 +296,20 @@ Devise.setup do |config| end config.omniauth :saml, saml_settings.merge(setup: ->(env) { OmniauthTenantSetup.saml(env) }) + config.omniauth :openid_connect, + name: :oidc, + scope: [:openid, :email, :profile], + response_type: :code, + issuer: Rails.application.secrets.oidc_issuer, + discovery: true, + client_auth_method: :basic, + client_options: { + identifier: Rails.application.secrets.oidc_client_id, + secret: Rails.application.secrets.oidc_client_secret, + redirect_uri: Rails.application.secrets.oidc_redirect_uri + }, + setup: ->(env) { OmniauthTenantSetup.oidc(env) } + # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 0c4347877..f4f37c0a9 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -282,6 +282,10 @@ en: sign_in: Sign in with SAML sign_up: Sign up with SAML name: SAML + oidc: + sign_in: Sign in with OIDC + sign_up: Sign up with OIDC + name: OIDC or_fill: "Or fill the following form:" proposals: create: diff --git a/config/locales/en/settings.yml b/config/locales/en/settings.yml index 065daad63..50ace1eb0 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" + oidc_login: "OpenID Connect login" + oidc_login_description: "Allow users to sign up with OpenID Connect (OIDC)" saml_login: "SAML login" saml_login_description: "Allow users to sign up with SAML" featured_proposals: "Featured proposals" diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index 2347e58e2..ca07be430 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -279,6 +279,10 @@ es: sign_in: Entra con SAML sign_up: Regístrate con SAML name: SAML + oidc: + sign_in: Entra con OIDC + sign_up: Regístrate con OIDC + name: OIDC 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 28f73ca30..02bb55f5c 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" + oidc_login: "Registro con OpenID Connect" + oidc_login_description: "Permitir que los usuarios se registren usando OpenID Connect (OIDC)" saml_login: "Registro con SAML" saml_login_description: "Permitir que los usuarios se registren usando SAML" featured_proposals: "Propuestas destacadas" diff --git a/config/secrets.yml.example b/config/secrets.yml.example index 4048b8486..0018d4986 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -94,6 +94,10 @@ staging: saml_sp_entity_id: "" saml_idp_metadata_url: "" saml_idp_sso_service_url: "" + oidc_client_id: "" + oidc_client_secret: "" + oidc_issuer: "" + oidc_redirect_uri: "" <<: *maps <<: *apis @@ -153,6 +157,10 @@ preproduction: saml_sp_entity_id: "" saml_idp_metadata_url: "" saml_idp_sso_service_url: "" + oidc_client_id: "" + oidc_client_secret: "" + oidc_issuer: "" + oidc_redirect_uri: "" <<: *maps <<: *apis @@ -211,5 +219,9 @@ production: saml_sp_entity_id: "" saml_idp_metadata_url: "" saml_idp_sso_service_url: "" + oidc_client_id: "" + oidc_client_secret: "" + oidc_issuer: "" + oidc_redirect_uri: "" <<: *maps <<: *apis diff --git a/docs/en/features/oauth.md b/docs/en/features/oauth.md index 9aa6593e1..8c5b71a81 100644 --- a/docs/en/features/oauth.md +++ b/docs/en/features/oauth.md @@ -1,10 +1,10 @@ # Authentication with external services (OAuth) -You can configure authentication services with external OAuth providers. Right now, Twitter, Facebook, Google, Wordpress and SAML are supported. +You can configure authentication services with external OAuth providers. Right now, Twitter, Facebook, Google, Wordpress, SAML and OpenID Connect (OIDC) are supported. ## 1. Create an App on the platform -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). +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). For OIDC, you'll need to register your application with an OpenID Connect provider. ## 2. Set the authentication URL of your Consul Democracy installation @@ -21,6 +21,8 @@ user_wordpress_oauth2_omniauth_authorize GET|POST /users/auth/wordpress_oauth2(. 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 +user_oidc_omniauth_authorize GET|POST /users/auth/oidc(.:format) users/omniauth_callbacks#passthru +user_oidc_omniauth_callback GET|POST /users/auth/oidc/callback(.:format) users/omniauth_callbacks#oidc ``` So for example the URL for Facebook application would be `yourdomain.com/users/auth/facebook/callback`. @@ -42,4 +44,8 @@ When you complete the application registration you'll get a *key* and *secret* v 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" + oidc_client_id: "your-oidc-client-id" + oidc_client_secret: "your-oidc-client-secret" + oidc_issuer: "https://your-oidc-provider.com" + oidc_redirect_uri: "https://yourapp.com/users/auth/oidc/callback" ``` diff --git a/docs/es/features/oauth.md b/docs/es/features/oauth.md index 62debfb90..eb718637a 100644 --- a/docs/es/features/oauth.md +++ b/docs/es/features/oauth.md @@ -1,10 +1,10 @@ # Autenticación con servicios externos (OAuth) -Puedes configurar la autenticación con servicios externos usando OAuth. Actualmente, se pueden utilizar Twitter, Facebook, Google, Wordpress y SAML. +Puedes configurar la autenticación con servicios externos usando OAuth. Actualmente, se pueden utilizar Twitter, Facebook, Google, Wordpress, SAML y OpenID Connect (OIDC). ## 1. Crea una aplicación en la plataforma -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). +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). Para OIDC, tendrás que registrar tu aplicación con un proveedor de OpenID Connect. ## 2. Establece la URL de autenticación de tu instalación de Consul Democracy @@ -21,6 +21,8 @@ user_wordpress_oauth2_omniauth_authorize GET|POST /users/auth/wordpress_oauth2(. 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 +user_oidc_omniauth_authorize GET|POST /users/auth/oidc(.:format) users/omniauth_callbacks#passthru +user_oidc_omniauth_callback GET|POST /users/auth/oidc/callback(.:format) users/omniauth_callbacks#oidc ``` Por ejemplo para Facebook la URL sería `yourdomain.com/users/auth/facebook/callback`. @@ -42,4 +44,8 @@ Cuando completes el registro de la aplicación en su plataforma te darán un *ke 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" + oidc_client_id: "tu-id-de-cliente-oidc" + oidc_client_secret: "tu-secreto-de-cliente-oidc" + oidc_issuer: "https://tu-proveedor-oidc.com" + oidc_redirect_uri: "https://tuaplicacion.com/users/auth/oidc/callback" ``` diff --git a/spec/components/devise/omniauth_form_component_spec.rb b/spec/components/devise/omniauth_form_component_spec.rb index f4e731206..ca6379081 100644 --- a/spec/components/devise/omniauth_form_component_spec.rb +++ b/spec/components/devise/omniauth_form_component_spec.rb @@ -10,6 +10,7 @@ describe Devise::OmniauthFormComponent do Setting["feature.google_login"] = false Setting["feature.wordpress_login"] = false Setting["feature.saml_login"] = false + Setting["feature.oidc_login"] = false end it "is not rendered when all authentications are disabled" do @@ -62,5 +63,14 @@ describe Devise::OmniauthFormComponent do expect(page).to have_button "SAML" expect(page).to have_button count: 1 end + + it "renders the OIDC link when the feature is enabled" do + Setting["feature.oidc_login"] = true + + render_inline component + + expect(page).to have_button "OIDC" + 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 index 77a9fff54..9b8929dd7 100644 --- a/spec/lib/omniauth_tenant_setup_spec.rb +++ b/spec/lib/omniauth_tenant_setup_spec.rb @@ -84,4 +84,96 @@ describe OmniauthTenantSetup do end end end + + describe "#oidc" do + it "uses different secrets for different tenants" do + create(:tenant, schema: "mars") + create(:tenant, schema: "venus") + + stub_secrets( + oidc_client_id: "default-client-id", + oidc_client_secret: "default-client-secret", + oidc_issuer: "https://default-oidc.example.com", + oidc_redirect_uri: "https://default.consul.dev/auth/oidc/callback", + tenants: { + mars: { + oidc_client_id: "mars-client-id", + oidc_client_secret: "mars-client-secret", + oidc_issuer: "https://mars-oidc.example.com", + oidc_redirect_uri: "https://mars.consul.dev/auth/oidc/callback" + }, + venus: { + oidc_client_id: "venus-client-id", + oidc_client_secret: "venus-client-secret", + oidc_issuer: "https://venus-oidc.example.com", + oidc_redirect_uri: "https://venus.consul.dev/auth/oidc/callback" + } + } + ) + + Tenant.switch("mars") do + mars_env = { + "omniauth.strategy" => double(options: {}), + "HTTP_HOST" => "mars.consul.dev" + } + + OmniauthTenantSetup.oidc(mars_env) + mars_strategy_options = mars_env["omniauth.strategy"].options + + expect(mars_strategy_options[:client_id]).to eq "mars-client-id" + expect(mars_strategy_options[:client_secret]).to eq "mars-client-secret" + expect(mars_strategy_options[:issuer]).to eq "https://mars-oidc.example.com" + expect(mars_strategy_options[:redirect_uri]).to eq "https://mars.consul.dev/auth/oidc/callback" + end + + Tenant.switch("venus") do + venus_env = { + "omniauth.strategy" => double(options: {}), + "HTTP_HOST" => "venus.consul.dev" + } + + OmniauthTenantSetup.oidc(venus_env) + venus_strategy_options = venus_env["omniauth.strategy"].options + + expect(venus_strategy_options[:client_id]).to eq "venus-client-id" + expect(venus_strategy_options[:client_secret]).to eq "venus-client-secret" + expect(venus_strategy_options[:issuer]).to eq "https://venus-oidc.example.com" + expect(venus_strategy_options[:redirect_uri]).to eq "https://venus.consul.dev/auth/oidc/callback" + end + end + + it "uses default secrets for non-overridden tenant" do + create(:tenant, schema: "earth") + + stub_secrets( + oidc_client_id: "default-client-id", + oidc_client_secret: "default-client-secret", + oidc_issuer: "https://default-oidc.example.com", + oidc_redirect_uri: "https://default.consul.dev/auth/oidc/callback", + tenants: { + mars: { + oidc_client_id: "mars-client-id", + oidc_client_secret: "mars-client-secret", + oidc_issuer: "https://mars-oidc.example.com", + oidc_redirect_uri: "https://mars.consul.dev/auth/oidc/callback" + } + } + ) + + Tenant.switch("earth") do + earth_env = { + "omniauth.strategy" => double(options: {}), + "HTTP_HOST" => "earth.consul.dev" + } + + OmniauthTenantSetup.oidc(earth_env) + earth_strategy_options = earth_env["omniauth.strategy"].options + + expect(earth_strategy_options[:client_id]).to eq "default-client-id" + expect(earth_strategy_options[:client_secret]).to eq "default-client-secret" + expect(earth_strategy_options[:issuer]).to eq "https://default-oidc.example.com" + expect(earth_strategy_options[:redirect_uri]).to eq "https://default.consul.dev/auth/oidc/callback" + end + end + end end diff --git a/spec/system/users_auth_spec.rb b/spec/system/users_auth_spec.rb index 61b4dbcf0..b02e8e878 100644 --- a/spec/system/users_auth_spec.rb +++ b/spec/system/users_auth_spec.rb @@ -645,6 +645,204 @@ describe "Users" do end end end + + context "OIDC" do + before { Setting["feature.oidc_login"] = true } + + let(:oidc_hash_unverified_email) do + { + provider: "oidc", + uid: "oidc-user-123", + info: { + name: "oidctester", + email: "tester@consul.dev" + } + } + end + + let(:oidc_hash_with_verified_email) do + { + provider: "oidc", + uid: "oidc-user-123", + info: { + name: "oidctester", + email: "tester@consul.dev", + verified: "1" + } + } + end + + scenario "Sign up with a confirmed email from OIDC provider" do + OmniAuth.config.add_mock(:oidc, oidc_hash_with_verified_email) + + visit new_user_registration_path + click_button "Sign up with OIDC" + + expect(page).to have_content "Successfully identified as Oidc" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "oidctester" + + click_link "Change my login details" + + expect(page).to have_field "Email", with: "tester@consul.dev" + end + + scenario "Sign up with an unconfirmed email from OIDC provider" do + OmniAuth.config.add_mock(:oidc, oidc_hash_unverified_email) + + visit new_user_registration_path + click_button "Sign up with OIDC" + + 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 OIDC" + + expect(page).to have_content "Successfully identified as Oidc" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "oidctester" + + click_link "Change my login details" + + expect(page).to have_field "Email", with: "tester@consul.dev" + end + + scenario "Sign in with a user with an OIDC identity" do + user = create(:user, username: "oidctester", email: "tester@consul.dev", password: "My123456") + create(:identity, uid: "oidc-user-123", provider: "oidc", user: user) + OmniAuth.config.add_mock(:oidc, { provider: "oidc", uid: "oidc-user-123" }) + + visit new_user_session_path + click_button "Sign in with OIDC" + + expect(page).to have_content "Successfully identified as Oidc" + expect_to_be_signed_in + + within("#notice") { click_button "Close" } + click_link "My account" + + expect(page).to have_field "Username", with: "oidctester" + + click_link "Change my login details" + + expect(page).to have_field "Email", with: "tester@consul.dev" + end + + scenario "Sign in with a user without an OIDC identity keeps the username" do + create(:user, username: "tester", email: "tester@consul.dev", password: "My123456") + OmniAuth.config.add_mock(:oidc, oidc_hash_with_verified_email) + + visit new_user_session_path + click_button "Sign in with OIDC" + + expect(page).to have_content "Successfully identified as Oidc" + 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 "OIDC 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.oidc_login"] = true } + end + + Tenant.switch("mars") do + mars_user = create(:user, username: "marsuser", email: "mars@consul.dev") + create(:identity, uid: "mars-oidc-123", provider: "oidc", user: mars_user) + end + + mars_oidc_hash = { + provider: "oidc", + uid: "mars-oidc-123", + info: { + name: "marsuser", + email: "mars@consul.dev" + } + } + + OmniAuth.config.add_mock(:oidc, mars_oidc_hash) + + with_subdomain("mars") do + visit new_user_session_path + click_button "Sign in with OIDC" + + expect(page).to have_content "Successfully identified as Oidc" + + 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 OIDC" + + expect(page).to have_content "To continue, please click on the confirmation " \ + "link that we have sent you via email" + end + end + + scenario "Allows user authentication when OIDC token has expired" do + user = create(:user, username: "oidctester", email: "tester@consul.dev") + create(:identity, uid: "oidc-user-123", provider: "oidc", user: user) + + expired_oidc_hash = oidc_hash_with_verified_email.merge( + credentials: { + token: "expired_token", + expires_at: 1.hour.ago.to_i, + expires: true + } + ) + + OmniAuth.config.add_mock(:oidc, expired_oidc_hash) + + visit new_user_session_path + click_button "Sign in with OIDC" + + expect(page).to have_content "Successfully identified as Oidc" + expect_to_be_signed_in + end + + scenario "Handle missing email claim from OIDC provider" do + oidc_hash_no_email = { + provider: "oidc", + uid: "oidc-user-no-email", + info: { + name: "noemailuser" + } + } + + OmniAuth.config.add_mock(:oidc, oidc_hash_no_email) + + visit new_user_registration_path + click_button "Sign up with OIDC" + + expect(page).to have_content "1 error prevented this Account from being saved." + + expect(page).to have_content "can't be blank" + end + end end scenario "Sign out" do