diff --git a/Gemfile b/Gemfile index 6d348bca0..a8f26cd6f 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'redcarpet' gem 'scrypt' gem 'omniauth' gem 'omniauth-rails_csrf_protection' +gem 'omniauth-saml' gem 'aws-sdk-codedeploy' gem 'aws-sdk-cloudwatchlogs' diff --git a/Gemfile.lock b/Gemfile.lock index 5d6531bb2..6091084b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -244,6 +244,9 @@ GEM omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) + omniauth-saml (2.1.0) + omniauth (~> 2.0) + ruby-saml (~> 1.12) orm_adapter (0.5.0) pg (1.2.3) pry (0.13.1) @@ -319,6 +322,9 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.10.2) + ruby-saml (1.16.0) + nokogiri (>= 1.13.10) + rexml ruby-vips (2.1.4) ffi (~> 1.12) rubyzip (2.3.2) @@ -422,6 +428,7 @@ DEPENDENCIES nokogiri omniauth omniauth-rails_csrf_protection + omniauth-saml pg pry puma (< 6) diff --git a/README.md b/README.md index 34c3f5efc..acc8392d0 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,6 @@ We recommend using [Docker Desktop][2] to get setup quickly. If you'd prefer not docker compose run --rm web rake db:setup ``` -### Create an admin user - -``` -docker compose run --rm web rake epets:add_sysadmin_user -``` - ### Load the country list ``` diff --git a/app/controllers/admin/omniauth_callbacks_controller.rb b/app/controllers/admin/omniauth_callbacks_controller.rb index 95ac35bbd..78a4fb705 100644 --- a/app/controllers/admin/omniauth_callbacks_controller.rb +++ b/app/controllers/admin/omniauth_callbacks_controller.rb @@ -1,25 +1,45 @@ class Admin::OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_before_action :require_admin - skip_before_action :verify_authenticity_token, only: %i[developer] + skip_before_action :verify_authenticity_token, only: %i[saml] - def developer - @user = AdminUser.find_by(email: omniauth_hash["uid"]) + rescue_from ActiveRecord::RecordNotFound do + redirect_to admin_login_url, alert: :login_failed + end + + def saml + @user = AdminUser.find_or_create_from!(provider, auth_data) if @user.present? - sign_in_and_redirect @user, event: :authentication - set_flash_message(:notice, :signed_in) if is_navigational_format? + sign_in @user, event: :authentication + + set_flash_message(:notice, :signed_in) + set_refresh_header + + render "admin/admin/index" else redirect_to admin_login_url, alert: :invalid_login end end + def failure + redirect_to admin_login_url, alert: :login_failed + end + private def after_omniauth_failure_path_for(scope) admin_login_url end - def omniauth_hash + def auth_data request.env["omniauth.auth"] end + + def provider + IdentityProvider.find_by!(name: auth_data.provider) + end + + def set_refresh_header + headers['Refresh'] = "0; url=#{admin_root_url}" + end end diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb index 31284179c..1df24f9e2 100644 --- a/app/controllers/admin/sessions_controller.rb +++ b/app/controllers/admin/sessions_controller.rb @@ -4,6 +4,14 @@ class Admin::SessionsController < Devise::SessionsController helper_method :last_request_at + def create + if provider? + redirect_to sso_provider_url(provider), status: :temporary_redirect + else + redirect_to admin_login_url, alert: :invalid_login + end + end + def continue respond_to do |format| format.json @@ -18,6 +26,20 @@ def status private + def email_domain + Mail::Address.new(sign_in_params[:email]).domain + rescue Mail::Field::ParseError + nil + end + + def provider + @provider ||= IdentityProvider.find_by(domain: email_domain) + end + + def provider? + provider.present? + end + def skip_timeout request.env['devise.skip_trackable'] = true end diff --git a/app/models/admin_user.rb b/app/models/admin_user.rb index f408fa6ea..e95e38c22 100644 --- a/app/models/admin_user.rb +++ b/app/models/admin_user.rb @@ -7,7 +7,7 @@ class AdminUser < ActiveRecord::Base class CannotDeleteCurrentUser < RuntimeError; end class MustBeAtLeastOneAdminUser < RuntimeError; end - devise :trackable, :timeoutable, :omniauthable, omniauth_providers: %i[developer] + devise :trackable, :timeoutable # TODO: Drop these columns once rollout of SSO has been completed self.ignored_columns = %i[ @@ -27,7 +27,7 @@ class MustBeAtLeastOneAdminUser < RuntimeError; end validates :first_name, :last_name, presence: true validates :email, presence: true, email: true validates :email, uniqueness: { case_sensitive: false } - validates :role, inclusion: { in: ROLES } + validates :role, presence: true, inclusion: { in: ROLES } # = Callbacks = before_save :reset_persistence_token, unless: :persistence_token? @@ -41,6 +41,26 @@ def self.timeout_in Site.login_timeout.seconds end + def self.find_or_create_from!(provider, auth_data) + find_or_create_by!(email: auth_data.fetch(:uid)) do |user| + user.first_name = auth_data.info.fetch(:first_name) + user.last_name = auth_data.info.fetch(:last_name) + groups = Array.wrap(auth_data.info.fetch(:groups)) + + if (groups & provider.sysadmins).any? + user.role = SYSADMIN_ROLE + elsif (groups & provider.moderators).any? + user.role = MODERATOR_ROLE + elsif (groups & provider.reviewers).any? + user.role = REVIEWER_ROLE + end + end + rescue ActiveRecord::RecordNotUnique => e + find_by!(email: auth_data.fetch(:uid)) + rescue ActiveRecord::RecordInvalid => e + Appsignal.send_exception(e) and return nil + end + def reset_persistence_token self.persistence_token = SecureRandom.hex(64) end diff --git a/app/models/identity_provider.rb b/app/models/identity_provider.rb new file mode 100644 index 000000000..67025cfb4 --- /dev/null +++ b/app/models/identity_provider.rb @@ -0,0 +1,100 @@ +class IdentityProvider + class ProviderNotFound < ArgumentError; end + + class << self + delegate :each, to: :providers + + def providers + @providers ||= load_providers + end + + def names + providers.map(&:name) + end + + def find_by(domain:) + providers.detect { |provider| provider.domains.include?(domain) } + end + + def find_by!(name:) + providers.detect { |provider| provider.name.to_s == name } || raise_provider_not_found(name) + end + + private + + def load_providers + Rails.application.config_for(:sso).map { |options| IdentityProvider.new(options) } + end + + def raise_provider_not_found(name) + raise ProviderNotFound, "Couldn't find the provider '#{name}'" + end + end + + attr_reader :name, :attribute_statements + attr_reader :assertion_consumer_service_url, :sp_entity_id + attr_reader :idp_sso_service_url, :idp_cert, :domains + attr_reader :sysadmins, :moderators, :reviewers + + def initialize(options) + @name = options.fetch(:name).to_sym + @attribute_statements = options.fetch(:attributes, default_attributes) + @assertion_consumer_service_url = "#{Site.moderate_url}/admin/auth/#{name}/callback" + @sp_entity_id = "#{Site.moderate_url}/admin/auth/#{name}" + @idp_sso_service_url = options.fetch(:idp_sso_service_url) + @idp_cert = options.fetch(:idp_cert, "") + @domains = options.fetch(:domains) + @sysadmins = options.fetch(:sysadmins, []) + @moderators = options.fetch(:moderators, []) + @reviewers = options.fetch(:reviewers, []) + + unless klass_defined? + strategies.const_set(klass, new_klass) + end + end + + def to_param + name.to_s + end + + def config + { + attribute_statements: attribute_statements, + assertion_consumer_service_url: assertion_consumer_service_url, + sp_entity_id: sp_entity_id, + idp_sso_service_url: idp_sso_service_url, + idp_cert: idp_cert + } + end + + private + + def default_attributes + { + email: ["email"], + first_name: ["first_name"], + last_name: ["last_name"], + groups: ["groups"] + } + end + + def strategies + OmniAuth::Strategies + end + + def parent_klass + OmniAuth::Strategies::SAML + end + + def new_klass + Class.new(parent_klass) + end + + def klass + @klass ||= name.to_s.camelize.to_sym + end + + def klass_defined? + strategies.const_defined?(klass, false) + end +end diff --git a/app/models/site.rb b/app/models/site.rb index 615d400e2..e56e4084b 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -86,6 +86,14 @@ def moderate_host_with_port instance.moderate_host_with_port end + def moderate_url + if table_exists? + instance.moderate_url + else + default_moderate_url + end + end + def constraints_for_moderation if table_exists? instance.constraints_for_moderation diff --git a/app/views/admin/sessions/new.html.erb b/app/views/admin/sessions/new.html.erb index cc337b429..3f80f9479 100644 --- a/app/views/admin/sessions/new.html.erb +++ b/app/views/admin/sessions/new.html.erb @@ -1,8 +1,13 @@

Sign in

- <%= form_tag(admin_auth_developer_path, method: :post) do %> - + <%= form_for(@user, as: :user, url: admin_login_path, authenticity_token: form_authenticity_token) do |form| %> +
+ <%= form.label :email, "Email", class: "form-label" %> + <%= form.text_field :email, class: "form-control", type: "email", autofocus: "autofocus" %> +
+ + <%= form.submit "Sign in", class: "button" %> <% end %>
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a3014e056..204e689f1 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -22,10 +22,6 @@ # ==> Navigation configuration config.sign_out_via = :get - # ==> Omniauth configuration - config.omniauth_path_prefix = '/admin/auth' - config.omniauth :developer, fields: %i[email] - # ==> Warden configuration # Reset the token after logging in so that other sessions are logged out Warden::Manager.after_set_user except: :fetch do |user, warden, options| diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index ed8872266..22bf0a143 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,3 +1,10 @@ -OmniAuth.configure do |config| - config.logger = Rails.logger +Rails.application.config.middleware.use OmniAuth::Builder do + configure do |config| + config.path_prefix = '/admin/auth' + config.logger = Rails.logger + end + + IdentityProvider.each do |idp| + provider idp.name, idp.config + end end diff --git a/config/locales/admin.en-GB.yml b/config/locales/admin.en-GB.yml index 501f50d66..abbaf34a5 100644 --- a/config/locales/admin.en-GB.yml +++ b/config/locales/admin.en-GB.yml @@ -74,6 +74,7 @@ en-GB: invalidation_started: "Enqueued the invalidation %{summary}" invalidation_updated: "Invalidation updated successfully" invalid_login: "Invalid login details" + login_failed: "There was a problem logging in - please contact support" logged_out: "You have been logged out" moderator_required: "You must be logged in as a moderator or system administrator to view this page" moderation_delay_sent: "An email has been sent to creators that moderation has been delayed" diff --git a/config/routes.rb b/config/routes.rb index 3a7af4515..e50ae9697 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -244,7 +244,6 @@ get '/', action: 'index', as: :stats post '/', action: 'create', as: nil end - end devise_for :users, class_name: 'AdminUser', module: 'admin', skip: %i[sessions] @@ -252,19 +251,25 @@ as :user do controller 'admin/sessions' do get '/admin/login', action: 'new' + post '/admin/login', action: 'create', as: nil get '/admin/logout', action: 'destroy' get '/admin/continue', action: 'continue' get '/admin/status', action: 'status' end + + controller 'admin/omniauth_callbacks' do + get '/admin/auth/failure', action: 'failure' + + scope '/admin/auth/:provider', via: %i[get post] do + match '/', action: 'passthru', as: :sso_provider + match '/callback', action: 'saml', as: :sso_provider_callback + end + end end end # Devise needs a `new_user_session_url` helper for its failure app direct(:new_user_session) { route_for(:admin_login) } - # Friendly url helpers for Omniauth - direct(:admin_auth_developer) { route_for(:user_developer_omniauth_authorize) } - direct(:admin_auth_developer_callback) { route_for(:user_developer_omniauth_callback) } - get 'ping', to: 'ping#ping' end diff --git a/config/sso.yml b/config/sso.yml new file mode 100644 index 000000000..e774e8c3e --- /dev/null +++ b/config/sso.yml @@ -0,0 +1,13 @@ +development: [] + +test: + - name: "example" + idp_sso_service_url: "http://localhost:3000/sso" + domains: + - "example.com" + sysadmins: + - "sysadmins" + moderators: + - "moderators" + reviewers: + - "reviewers" diff --git a/features/admin/admin_access.feature b/features/admin/admin_access.feature index 7bbafbb2a..1c44c96c0 100644 --- a/features/admin/admin_access.feature +++ b/features/admin/admin_access.feature @@ -15,12 +15,12 @@ Feature: Restricted access to the admin console And I should be connected to the server via an ssl connection And the markup should be valid + @javascript Scenario: Login and logout to the admin console as a sysadmin - Given a sysadmin user exists with email: "admin@example.com", first_name: "John", last_name: "Admin" + Given a sysadmin SSO user exists When I go to the admin login page - And I press "Login with developer strategy" - And I fill in "Email" with "admin@example.com" - And I press "Sign In" + And I fill in "Email" with "sysadmin@example.com" + And I press "Sign in" Then I should be on the admin home page And I should be connected to the server via an ssl connection And the markup should be valid @@ -29,22 +29,54 @@ Feature: Restricted access to the admin console And I follow "Logout" And I should be on the admin login page - Scenario: Login and logout to the admin console as a moderator user - Given a moderator user exists with email: "admin@example.com", first_name: "John", last_name: "Moderator" + @javascript + Scenario: Login and logout to the admin console as a moderator + Given a moderator SSO user exists When I go to the admin login page - And I press "Login with developer strategy" - And I fill in "Email" with "admin@example.com" - And I press "Sign In" + And I fill in "Email" with "moderator@example.com" + And I press "Sign in" Then I should be on the admin home page + And I should be connected to the server via an ssl connection + And the markup should be valid + And I should not see "Users" And I should see "John Moderator" + And I follow "Logout" + And I should be on the admin login page + + @javascript + Scenario: Login and logout to the admin console as a reviewer + Given a reviewer SSO user exists + When I go to the admin login page + And I fill in "Email" with "reviewer@example.com" + And I press "Sign in" + Then I should be on the admin home page + And I should be connected to the server via an ssl connection + And the markup should be valid And I should not see "Users" + And I should see "John Reviewer" And I follow "Logout" And I should be on the admin login page + Scenario: Invalid domain + When I go to the admin login page + And I fill in "Email" with "admin@example.org" + And I press "Sign in" + Then I should see "Invalid login details" + And should not see "Logout" + Scenario: Invalid login - Given I go to the admin login page - And I press "Login with developer strategy" + Given an invalid SSO login + When I go to the admin login page And I fill in "Email" with "admin@example.com" - And I press "Sign In" + And I press "Sign in" + Then I should see "There was a problem logging in - please contact support" + And should not see "Logout" + + Scenario: Valid login but no role + Given a valid SSO login with no roles + When I go to the admin login page + And I fill in "Email" with "norole@example.com" + And I press "Sign in" Then I should see "Invalid login details" And should not see "Logout" + diff --git a/features/admin/admin_users.feature b/features/admin/admin_users.feature index 4f9ce4b22..de406a8b8 100644 --- a/features/admin/admin_users.feature +++ b/features/admin/admin_users.feature @@ -5,7 +5,7 @@ Feature: Admin users index and crud Background: Given the time is "Sun, 17 Dec 2023 08:28:41 +0000" - And I am logged in as a sysadmin with the email "muddy@fox.com", first_name "Sys", last_name "Admin" + And I am logged in as a sysadmin with the email "muddy@example.com", first_name "Sys", last_name "Admin" And a moderator user exists with email: "naomi@example.com", first_name: "Naomi", last_name: "Campbell" Scenario: Accessing the admin users index @@ -20,7 +20,7 @@ Feature: Admin users index and crud When I go to the admin users index page Then I should see the following admin user table: | Name | Email | Role | Last login | - | Admin, Sys | muddy@fox.com | sysadmin | 08:28am on 17 December 2023 | + | Admin, Sys | muddy@example.com | sysadmin | 08:28am on 17 December 2023 | | Campbell, Naomi | naomi@example.com | moderator | | | Hunt, Helen | helen@example.com | moderator | 10:09am on 15 December 2023 | | Jacobi, Derek | derek@example.com | moderator | 14:27pm on 11 December 2023 | diff --git a/features/admin/anonymize_petitions.feature b/features/admin/anonymize_petitions.feature index 46fc4ad31..5457e8f16 100644 --- a/features/admin/anonymize_petitions.feature +++ b/features/admin/anonymize_petitions.feature @@ -4,7 +4,7 @@ Feature: An admin anonymizes petitions I want to anonymize all petitions 6 months after parliament closes in accordance with our privacy policy Background: - Given I am logged in as a sysadmin with the email "muddy@fox.com", first_name "Sys", last_name "Admin" + Given I am logged in as a sysadmin with the email "muddy@example.com", first_name "Sys", last_name "Admin" @javascript Scenario: Admin anonymizes petitions 6 months after parliament closes diff --git a/features/step_definitions/authentication_steps.rb b/features/step_definitions/authentication_steps.rb index f8c2fcd60..fc0cf7299 100644 --- a/features/step_definitions/authentication_steps.rb +++ b/features/step_definitions/authentication_steps.rb @@ -1,15 +1,15 @@ Given /^I am logged in as a sysadmin$/ do - @user = FactoryBot.create(:sysadmin_user) + @user = FactoryBot.build(:sysadmin_sso_user) step "the admin user is logged in" end Given /^I am logged in as a moderator$/ do - @user = FactoryBot.create(:moderator_user) + @user = FactoryBot.build(:moderator_sso_user) step "the admin user is logged in" end Given(/^I log out and login back in as a sysadmin$/) do - @user = FactoryBot.create(:sysadmin_user) + @user = FactoryBot.build(:sysadmin_sso_user) steps %q[ the admin user is logged out @@ -18,7 +18,7 @@ end Given(/^I log out and login back in as a moderator$/) do - @user = FactoryBot.create(:moderator_user) + @user = FactoryBot.build(:moderator_sso_user) steps %q[ the admin user is logged out @@ -28,34 +28,49 @@ Given /^I am logged in as a moderator named "([^\"]*)"$/ do |name| first_name, last_name = name.split - @user = FactoryBot.create(:moderator_user, first_name: first_name, last_name: last_name) - step "the admin user is logged in" -end - -Given /^I am logged in as a moderator named "([^\"]*)" with the password "([^\"]*)"$/ do |name, password| - first_name, last_name = name.split - @user = FactoryBot.create(:moderator_user, first_name: first_name, last_name: last_name, :password => password, :password_confirmation => password) + @user = FactoryBot.build(:moderator_sso_user, first_name: first_name, last_name: last_name) step "the admin user is logged in" end Given /^I am logged in as a sysadmin named "([^\"]*)"$/ do |name| first_name, last_name = name.split - @user = FactoryBot.create(:sysadmin_user, first_name: first_name, last_name: last_name) + @user = FactoryBot.build(:sysadmin_sso_user, first_name: first_name, last_name: last_name) step "the admin user is logged in" end Given /^I am logged in as a sysadmin with the email "([^\"]*)", first_name "([^\"]*)", last_name "([^\"]*)"$/ do |email, first_name, last_name| - @user = FactoryBot.create(:sysadmin_user, :email => email, :first_name => first_name, :last_name => last_name) + @user = FactoryBot.build(:sysadmin_sso_user, email: email, first_name: first_name, last_name: last_name) step "the admin user is logged in" end Given /^the admin user is logged in$/ do + OmniAuth.config.mock_auth[:example] = @user + visit admin_login_url - click_button("Login with developer strategy") - fill_in("Email", :with => @user.email) - click_button("Sign In") + fill_in("Email", with: @user.uid) + click_button("Sign in") end Given /^the admin user is logged out$/ do visit admin_logout_url end + +Given /^a sysadmin SSO user exists$/ do + OmniAuth.config.mock_auth[:example] = FactoryBot.build(:sysadmin_sso_user) +end + +Given /^a moderator SSO user exists$/ do + OmniAuth.config.mock_auth[:example] = FactoryBot.build(:moderator_sso_user) +end + +Given /^a reviewer SSO user exists$/ do + OmniAuth.config.mock_auth[:example] = FactoryBot.build(:reviewer_sso_user) +end + +Given(/^a valid SSO login with no roles$/) do + OmniAuth.config.mock_auth[:example] = FactoryBot.build(:norole_sso_user) +end + +Given /^an invalid SSO login$/ do + OmniAuth.config.mock_auth[:example] = :invalid_credentials +end diff --git a/features/support/hooks.rb b/features/support/hooks.rb index 103d1aadf..1d3405adc 100644 --- a/features/support/hooks.rb +++ b/features/support/hooks.rb @@ -83,3 +83,16 @@ Before do Rails.application.env_config['action_dispatch.show_detailed_exceptions'] = false end + +Before do + OmniAuth.config.test_mode = true + + OmniAuth.config.on_failure = Proc.new { |env| + OmniAuth::FailureEndpoint.new(env).redirect_to_failure + } +end + +After do + OmniAuth.config.mock_auth[:example] = nil + OmniAuth.config.test_mode = false +end diff --git a/lib/package_builder/scripts/after_install.sh b/lib/package_builder/scripts/after_install.sh index edf20d66e..2e3d18ebc 100644 --- a/lib/package_builder/scripts/after_install.sh +++ b/lib/package_builder/scripts/after_install.sh @@ -5,6 +5,7 @@ set -o pipefail chown -R deploy:deploy /home/deploy/epetitions/releases/<%= release %> su - deploy <<'EOF' +ln -nfs /home/deploy/epetitions/shared/config/sso.yml /home/deploy/epetitions/releases/<%= release %>/config/sso.yml ln -nfs /home/deploy/epetitions/shared/tmp /home/deploy/epetitions/releases/<%= release %>/tmp ln -nfs /home/deploy/epetitions/shared/log /home/deploy/epetitions/releases/<%= release %>/log ln -nfs /home/deploy/epetitions/shared/bundle /home/deploy/epetitions/releases/<%= release %>/vendor/bundle diff --git a/spec/factories.rb b/spec/factories.rb index 1fe7cc448..7ba8c74db 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -982,4 +982,82 @@ ) end end + + factory :sso_user, class: Hash do + transient do + email { "admin@example.com" } + first_name { "Sys" } + last_name { "Admin" } + groups { %w[sysadmins] } + end + + provider { "example" } + uid { email } + + info do + { + name: "#{first_name} #{last_name}", + email: email, + first_name: first_name, + last_name: last_name, + groups: groups + } + end + + credentials do + {} + end + + extra do + { + raw_info: { + name: "#{first_name} #{last_name}", + email: email, + first_name: first_name, + last_name: last_name, + groups: groups + } + } + end + + skip_create + + initialize_with { OmniAuth::AuthHash.new(attributes) } + end + + factory :sysadmin_sso_user, parent: :sso_user do + transient do + email { "sysadmin@example.com" } + first_name { "John" } + last_name { "Admin" } + groups { %w[sysadmins] } + end + end + + factory :moderator_sso_user, parent: :sso_user do + transient do + email { "moderator@example.com" } + first_name { "John" } + last_name { "Moderator" } + groups { %w[moderators] } + end + end + + factory :reviewer_sso_user, parent: :sso_user do + transient do + email { "reviewer@example.com" } + first_name { "John" } + last_name { "Reviewer" } + groups { %w[reviewers] } + end + end + + factory :norole_sso_user, parent: :sso_user do + transient do + email { "norole@example.com" } + first_name { "No" } + last_name { "Role" } + groups { %w[] } + end + end end diff --git a/spec/requests/admin_user_persistence_token_spec.rb b/spec/requests/admin_user_persistence_token_spec.rb index 8c3679133..5dac1d99e 100644 --- a/spec/requests/admin_user_persistence_token_spec.rb +++ b/spec/requests/admin_user_persistence_token_spec.rb @@ -5,16 +5,17 @@ { first_name: "System", last_name: "Administrator", - email: "admin@petition.parliament.uk" + email: "admin@example.com" } end let(:login_params) do - { email: "admin@petition.parliament.uk" } + { email: "admin@example.com" } end before do - FactoryBot.create(:sysadmin_user, user_attributes) + sso_user = FactoryBot.create(:sysadmin_sso_user, **user_attributes) + OmniAuth.config.mock_auth[:example] = sso_user end def new_browser @@ -28,16 +29,36 @@ def new_browser context "when a new session is created" do it "logs out existing sessions" do s1 = new_browser - s1.post "/admin/auth/developer/callback", params: login_params + s1.post "/admin/login", params: { user: login_params } + + expect(s1.response.status).to eq(307) + expect(s1.response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example") + + s1.follow_redirect!(params: s1.request.POST) expect(s1.response.status).to eq(302) - expect(s1.response.location).to eq("https://moderate.petition.parliament.uk/admin") + expect(s1.response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example/callback") + + s1.follow_redirect! + + expect(s1.response.status).to eq(200) + expect(s1.response).to have_header("Refresh", "0; url=https://moderate.petition.parliament.uk/admin") s2 = new_browser - s2.post "/admin/auth/developer/callback", params: login_params + s2.post "/admin/login", params: { user: login_params } + + expect(s2.response.status).to eq(307) + expect(s2.response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example") + + s2.follow_redirect!(params: s2.request.POST) expect(s2.response.status).to eq(302) - expect(s2.response.location).to eq("https://moderate.petition.parliament.uk/admin") + expect(s2.response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example/callback") + + s2.follow_redirect! + + expect(s2.response.status).to eq(200) + expect(s2.response).to have_header("Refresh", "0; url=https://moderate.petition.parliament.uk/admin") s1.get("/admin") @@ -49,10 +70,20 @@ def new_browser context "when a session is destroyed" do it "resets the persistence token" do s1 = new_browser - s1.post "/admin/auth/developer/callback", params: login_params + s1.post "/admin/login", params: { user: login_params } + + expect(s1.response.status).to eq(307) + expect(s1.response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example") + + s1.follow_redirect!(params: s1.request.POST) expect(s1.response.status).to eq(302) - expect(s1.response.location).to eq("https://moderate.petition.parliament.uk/admin") + expect(s1.response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example/callback") + + s1.follow_redirect! + + expect(s1.response.status).to eq(200) + expect(s1.response).to have_header("Refresh", "0; url=https://moderate.petition.parliament.uk/admin") s2 = new_browser s2.cookies["_epets_session"] = s1.cookies["_epets_session"] @@ -79,8 +110,20 @@ def new_browser Site.instance.update(login_timeout: 600) travel_to 5.minutes.ago do - post "/admin/auth/developer/callback", params: login_params - expect(response).to redirect_to("/admin") + post "/admin/login", params: { user: login_params } + + expect(response.status).to eq(307) + expect(response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example") + + follow_redirect!(params: request.POST) + + expect(response.status).to eq(302) + expect(response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example/callback") + + follow_redirect! + + expect(response.status).to eq(200) + expect(response).to have_header("Refresh", "0; url=https://moderate.petition.parliament.uk/admin") end get "/admin" diff --git a/spec/requests/csrf_token_spec.rb b/spec/requests/csrf_token_spec.rb index b2b3e308d..4e94edbab 100644 --- a/spec/requests/csrf_token_spec.rb +++ b/spec/requests/csrf_token_spec.rb @@ -5,12 +5,17 @@ { first_name: "System", last_name: "Administrator", - email: "admin@petition.parliament.uk" + email: "admin@example.com" } end let(:login_params) do - { email: "admin@petition.parliament.uk" } + { email: "admin@example.com" } + end + + before do + sso_user = FactoryBot.create(:sysadmin_sso_user, **user_attributes) + OmniAuth.config.mock_auth[:example] = sso_user end let(:encrypted_csrf_token) do @@ -31,20 +36,36 @@ get "/admin/login" end + def do_login + post "/admin/login", params: { user: login_params, authenticity_token: authenticity_token } + + expect(response.status).to eq(307) + expect(response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example") + + follow_redirect!(params: request.POST) + + expect(response.status).to eq(302) + expect(response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example/callback") + + follow_redirect! + + expect(response.status).to eq(200) + expect(response).to have_header("Refresh", "0; url=https://moderate.petition.parliament.uk/admin") + end + context "when a new session is created" do it "resets the csrf token" do expect { - post "/admin/auth/developer/callback", params: login_params + do_login }.to change { session[:_csrf_token] - }.from(be_present).to(be_nil) + } end end context "when a session is destroyed" do before do - post "/admin/auth/developer/callback", params: login_params - follow_redirect! + do_login end it "resets the csrf token" do @@ -61,8 +82,7 @@ Site.instance.update(login_timeout: 600) travel_to 5.minutes.ago do - post "/admin/auth/developer/callback", params: login_params - expect(response).to redirect_to("/admin") + do_login end get "/admin" diff --git a/spec/requests/login_timeout_spec.rb b/spec/requests/login_timeout_spec.rb index 8973298f9..89ff55fec 100644 --- a/spec/requests/login_timeout_spec.rb +++ b/spec/requests/login_timeout_spec.rb @@ -5,15 +5,18 @@ { first_name: "System", last_name: "Administrator", - email: "admin@petition.parliament.uk" + email: "admin@example.com" } end let(:login_params) do - { email: "admin@petition.parliament.uk" } + { email: "admin@example.com" } end - let!(:user) { FactoryBot.create(:sysadmin_user, user_attributes) } + before do + sso_user = FactoryBot.create(:sysadmin_sso_user, **user_attributes) + OmniAuth.config.mock_auth[:example] = sso_user + end before do host! "moderate.petition.parliament.uk" @@ -24,8 +27,20 @@ Site.instance.update(login_timeout: 600) travel_to 2.minutes.ago do - post "/admin/auth/developer/callback", params: login_params - expect(response).to redirect_to("/admin") + post "/admin/login", params: { user: login_params } + + expect(response.status).to eq(307) + expect(response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example") + + follow_redirect!(params: request.POST) + + expect(response.status).to eq(302) + expect(response.location).to eq("https://moderate.petition.parliament.uk/admin/auth/example/callback") + + follow_redirect! + + expect(response.status).to eq(200) + expect(response).to have_header("Refresh", "0; url=https://moderate.petition.parliament.uk/admin") end get "/admin" diff --git a/spec/support/omniauth.rb b/spec/support/omniauth.rb new file mode 100644 index 000000000..841ce48f0 --- /dev/null +++ b/spec/support/omniauth.rb @@ -0,0 +1,16 @@ +RSpec.configure do |config| + config.around(:each, type: :request) do |example| + OmniAuth.config.test_mode = true + existing_failure_proc = OmniAuth.config.on_failure + + OmniAuth.config.on_failure = Proc.new { |env| + OmniAuth::FailureEndpoint.new(env).redirect_to_failure + } + + example.run + ensure + OmniAuth.config.mock_auth[:example] = nil + OmniAuth.config.on_failure = existing_failure_proc + OmniAuth.config.test_mode = false + end +end