From 91a10812eb468505f83e672fb7d32ca32025ea8d Mon Sep 17 00:00:00 2001 From: CatalinVoineag <11318084+CatalinVoineag@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:38:36 +0000 Subject: [PATCH] One login auth We want to move our candidates to use one login when sigin up/in to our service. This commit adds the basic implementation for users to login or create a candidate account in apply using one login. --- app/controllers/one_login_controller.rb | 89 ++++++++ app/models/one_login_user.rb | 50 +++++ app/models/one_login_user_bypass.rb | 30 +++ .../create_account_or_sign_in.html.erb | 50 +++-- app/views/layouts/_header.html.erb | 3 +- config/application.rb | 2 +- config/initializers/omniauth.rb | 22 ++ config/locales/en.yml | 1 + config/routes.rb | 8 +- lib/omniauth/onelogin_setup.rb | 41 ++++ .../strategies/one_login_developer.rb | 9 + spec/factories/one_login_auth.rb | 7 + spec/models/one_login_user_bypass_spec.rb | 38 ++++ spec/models/one_login_user_spec.rb | 68 ++++++ spec/requests/one_login_controller_spec.rb | 210 ++++++++++++++++++ spec/support/test_helpers/one_login_helper.rb | 16 ++ .../candidate_signs_in_spec.rb | 92 ++++++++ 17 files changed, 714 insertions(+), 22 deletions(-) create mode 100644 app/controllers/one_login_controller.rb create mode 100644 app/models/one_login_user.rb create mode 100644 app/models/one_login_user_bypass.rb create mode 100644 lib/omniauth/onelogin_setup.rb create mode 100644 lib/omniauth/strategies/one_login_developer.rb create mode 100644 spec/factories/one_login_auth.rb create mode 100644 spec/models/one_login_user_bypass_spec.rb create mode 100644 spec/models/one_login_user_spec.rb create mode 100644 spec/requests/one_login_controller_spec.rb create mode 100644 spec/support/test_helpers/one_login_helper.rb create mode 100644 spec/system/candidate_interface/one_login_signup/candidate_signs_in_spec.rb diff --git a/app/controllers/one_login_controller.rb b/app/controllers/one_login_controller.rb new file mode 100644 index 00000000000..112b36617d7 --- /dev/null +++ b/app/controllers/one_login_controller.rb @@ -0,0 +1,89 @@ +class OneLoginController < ApplicationController + before_action :one_login_enabled + before_action :set_sentry_context, only: :callback + + def callback + auth = request.env['omniauth.auth'] + session[:onelogin_id_token] = auth&.credentials&.id_token + candidate = OneLoginUser.authentificate(auth) + + sign_in(candidate, scope: :candidate) + candidate.update!(last_signed_in_at: Time.zone.now) + + redirect_to candidate_interface_interstitial_path + rescue OneLoginUser::Error => e + session[:one_login_error] = e.message + redirect_to auth_onelogin_sign_out_path + end + + def bypass_callback + one_login_user_bypass = OneLoginUserBypass.new( + token: request.env['omniauth.auth']&.uid, + ) + candidate = one_login_user_bypass.authentificate + + if one_login_user_bypass.valid? && candidate.present? + sign_in(candidate, scope: :candidate) + candidate.update!(last_signed_in_at: Time.zone.now) + + redirect_to candidate_interface_interstitial_path + else + flash[:warning] = one_login_user_bypass.errors.full_messages.join('\n') + redirect_to candidate_interface_create_account_or_sign_in_path + end + end + + def sign_out + id_token = session[:onelogin_id_token] + one_login_error = session[:one_login_error] + reset_session + + session[:one_login_error] = one_login_error + if OneLogin.bypass? + redirect_to candidate_interface_create_account_or_sign_in_path + else + # Go back to one login to sign out the user on their end as well + redirect_to logout_onelogin(id_token), allow_other_host: true + end + end + + def sign_out_complete + if session[:one_login_error].present? + Sentry.capture_message(session[:one_login_error], level: :error) + redirect_to internal_server_error_path + else + redirect_to candidate_interface_create_account_or_sign_in_path + end + end + + def failure + session[:one_login_error] = "One login failure with #{params[:message]} " \ + "for onelogin_id_token: #{session[:onelogin_id_token]}" + + redirect_to auth_onelogin_sign_out_path + end + +private + + def one_login_enabled + return if FeatureFlag.active?(:one_login_candidate_sign_in) + + redirect_to root_path + end + + def set_sentry_context + Sentry.set_extras( + omniauth_hash: request.env['omniauth.auth'].to_h, + ) + end + + def logout_onelogin(id_token_hint) + params = { + post_logout_redirect_uri: URI(auth_onelogin_sign_out_complete_url), + id_token_hint:, + } + URI.parse("#{ENV['GOVUK_ONE_LOGIN_ISSUER_URL']}logout").tap do |uri| + uri.query = URI.encode_www_form(params) + end.to_s + end +end diff --git a/app/models/one_login_user.rb b/app/models/one_login_user.rb new file mode 100644 index 00000000000..f41cbccc2ed --- /dev/null +++ b/app/models/one_login_user.rb @@ -0,0 +1,50 @@ +class OneLoginUser + class Error < StandardError; end + attr_reader :email_address, :token + + def initialize(omniauth_object) + @email_address = omniauth_object.info.email + @token = omniauth_object.uid + end + + def self.authentificate(omniauth_auth) + new(omniauth_auth).authentificate + end + + def authentificate + one_login_auth = OneLoginAuth.find_by(token:) + existing_candidate = Candidate.find_by(email_address:) + + return candidate_with_one_login(one_login_auth) if one_login_auth + return existing_candidate_without_one_login(existing_candidate) if existing_candidate + + created_candidate + end + +private + + def candidate_with_one_login(one_login_auth) + one_login_auth.update!(email_address:) + one_login_auth.candidate + end + + def existing_candidate_without_one_login(existing_candidate) + if existing_candidate.one_login_auth.present? && existing_candidate.one_login_auth.token != token + raise( + Error, + "Candidate #{existing_candidate.id} has a different one login " \ + "token than the user trying to login. Token used to auth #{token}", + ) + end + + existing_candidate.create_one_login_auth!(token:, email_address:) + existing_candidate + end + + def created_candidate + candidate = Candidate.create!(email_address:) + candidate.create_one_login_auth!(token:, email_address:) + + candidate + end +end diff --git a/app/models/one_login_user_bypass.rb b/app/models/one_login_user_bypass.rb new file mode 100644 index 00000000000..a99e6ef68e1 --- /dev/null +++ b/app/models/one_login_user_bypass.rb @@ -0,0 +1,30 @@ +class OneLoginUserBypass + include ActiveModel::Model + + validates :token, presence: true + + attr_accessor :token + + def authentificate + return unless valid? + + one_login_auth = OneLoginAuth.find_by(token:) + + return one_login_auth.candidate if one_login_auth + + created_candidate + end + +private + + def created_candidate + candidate = Candidate.create!(email_address: bypass_email_address) + candidate.create_one_login_auth!(token:, email_address: bypass_email_address) + + candidate + end + + def bypass_email_address + "#{token}@example.com" + end +end diff --git a/app/views/candidate_interface/start_page/create_account_or_sign_in.html.erb b/app/views/candidate_interface/start_page/create_account_or_sign_in.html.erb index 350a6fddd86..1581df129d2 100644 --- a/app/views/candidate_interface/start_page/create_account_or_sign_in.html.erb +++ b/app/views/candidate_interface/start_page/create_account_or_sign_in.html.erb @@ -4,31 +4,43 @@
- <%= form_with( - model: @create_account_or_sign_in_form, - url: candidate_interface_create_account_or_sign_in_path(providerCode: params[:providerCode], courseCode: params[:courseCode]), - method: :post, - ) do |f| %> - <%= f.govuk_error_summary %> - + <% if FeatureFlag.active?(:one_login_candidate_sign_in) %>

<%= t('page_titles.create_account_or_sign_in') %>

- <%= f.govuk_radio_buttons_fieldset :existing_account, legend: { text: 'Do you already have an account?' } do %> - <%= f.govuk_radio_button :existing_account, true, label: { text: 'Yes, sign in' }, link_errors: true do %> - <%= f.govuk_email_field :email, label: { text: 'Email address', size: 's' }, hint: { text: 'Enter the email address you used to register, and we will send you a link to sign in.' }, width: 'two-thirds', autocomplete: 'email', spellcheck: false %> +

+ <%= t('govuk.one_login_account_guidance') %> +

+ + <%= govuk_button_to(t('continue'), OneLogin.bypass? ? '/auth/one-login-developer' : '/auth/onelogin') %> + <% else %> + <%= form_with( + model: @create_account_or_sign_in_form, + url: candidate_interface_create_account_or_sign_in_path(providerCode: params[:providerCode], courseCode: params[:courseCode]), + method: :post, + ) do |f| %> + <%= f.govuk_error_summary %> + +

+ <%= t('page_titles.create_account_or_sign_in') %> +

+ + <%= f.govuk_radio_buttons_fieldset :existing_account, legend: { text: 'Do you already have an account?' } do %> + <%= f.govuk_radio_button :existing_account, true, label: { text: 'Yes, sign in' }, link_errors: true do %> + <%= f.govuk_email_field :email, label: { text: 'Email address', size: 's' }, hint: { text: 'Enter the email address you used to register, and we will send you a link to sign in.' }, width: 'two-thirds', autocomplete: 'email', spellcheck: false %> + <% end %> + <%= f.govuk_radio_button :existing_account, false, label: { text: 'No, I need to create an account' } %> <% end %> - <%= f.govuk_radio_button :existing_account, false, label: { text: 'No, I need to create an account' } %> + <%= f.govuk_submit t('continue') %> <% end %> - <%= f.govuk_submit t('continue') %> - <% end %> -

- You can usually start applying for teacher training in October, the - year before your course starts. Courses can fill up quickly, so apply - as soon as you can. - <%= govuk_link_to 'Read how the application process works', candidate_interface_guidance_path %>. -

+

+ You can usually start applying for teacher training in October, the + year before your course starts. Courses can fill up quickly, so apply + as soon as you can. + <%= govuk_link_to 'Read how the application process works', candidate_interface_guidance_path %>. +

+ <% end %>
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 3deb9e3ea19..dbbaee47aad 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -7,9 +7,10 @@ )) %> <%= render PhaseBannerComponent.new(no_border: current_candidate.present?) %> <% if current_candidate %> + <% sign_out_path = FeatureFlag.active?(:one_login_candidate_sign_in) ? auth_onelogin_sign_out_path : candidate_interface_sign_out_path %> <%= render(PrimaryNavigationComponent.new( items: NavigationItems.candidate_primary_navigation(current_candidate:, current_controller: controller), - items_right: [NavigationItems::NavigationItem.new('Sign out', candidate_interface_sign_out_path)], + items_right: [NavigationItems::NavigationItem.new('Sign out', sign_out_path)], )) %> <% end %> <% when 'support_interface' %> diff --git a/config/application.rb b/config/application.rb index 369d73df784..06166cab23b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -40,7 +40,7 @@ class Application < Rails::Application # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. - config.autoload_lib(ignore: %w[assets tasks generators rubocop]) + config.autoload_lib(ignore: %w[assets tasks generators rubocop omniauth]) # Configuration for the application, engines, and railties goes here. # diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 0e26334e100..14d1dceada8 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,4 +1,6 @@ OmniAuth.config.logger = Rails.logger +require 'omniauth/strategies/one_login_developer' +require 'omniauth/onelogin_setup' dfe_sign_in_identifier = ENV['DFE_SIGN_IN_CLIENT_ID'] dfe_sign_in_secret = ENV['DFE_SIGN_IN_SECRET'] @@ -32,6 +34,12 @@ def self.bypass? end end +module ::OneLogin + def self.bypass? + HostingEnvironment.review? || HostingEnvironment.loadtest? || Rails.env.development? + end +end + if DfESignIn.bypass? Rails.application.config.middleware.use OmniAuth::Builder do provider :developer, @@ -41,3 +49,17 @@ def self.bypass? else Rails.application.config.middleware.use OmniAuth::Strategies::OpenIDConnect, options end + +if OneLogin.bypass? + Rails.application.config.middleware.use OmniAuth::Builder do + provider :one_login_developer, + request_path: '/auth/one-login-developer', + callback_path: '/auth/one-login-developer/callback', + fields: %i[uid], + uid_field: :uid + end +else + Rails.application.config.middleware.use OmniAuth::Builder do |builder| + OneloginSetup.configure(builder) + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ee504b6909..aeecfe8dde3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -24,6 +24,7 @@ en: national_careers_service: find_a_course_url: https://nationalcareers.service.gov.uk/find-a-course/search govuk: + one_login_account_guidance: You need a GOV.UK One Login to sign in to this service. You can create one if you do not already have one. url: https://www.gov.uk terms_conditions_url: https://www.gov.uk/help/terms-conditions safeguarding: https://www.gov.uk/tell-employer-or-college-about-criminal-record diff --git a/config/routes.rb b/config/routes.rb index f2137a1220b..f85b6aeaf2f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,6 +23,12 @@ get '/auth/developer/callback' => 'dfe_sign_in#bypass_callback' get '/auth/dfe/sign-out' => 'dfe_sign_in#redirect_after_dsi_signout' + get '/auth/onelogin/callback', to: 'one_login#callback' + get '/auth/one-login-developer/callback' => 'one_login#bypass_callback' + get '/auth/onelogin/sign-out', to: 'one_login#sign_out' + get '/auth/onelogin/sign-out-complete', to: 'one_login#sign_out_complete' + get '/auth/failure', to: 'one_login#failure' + direct :find do if HostingEnvironment.sandbox_mode? I18n.t('find_teacher_training.sandbox_url') @@ -56,6 +62,6 @@ get '/404', to: 'errors#not_found' get '/406', to: 'errors#not_acceptable' get '/422', to: 'errors#unprocessable_entity' - get '/500', to: 'errors#internal_server_error' + get '/500', to: 'errors#internal_server_error', as: :internal_server_error end end diff --git a/lib/omniauth/onelogin_setup.rb b/lib/omniauth/onelogin_setup.rb new file mode 100644 index 00000000000..ff3048bb1ed --- /dev/null +++ b/lib/omniauth/onelogin_setup.rb @@ -0,0 +1,41 @@ +module OneloginSetup + class OmniAuth::Strategies::GovukOneLogin < OmniAuth::Strategies::OpenIDConnect; end + + def self.configure(builder) + client_id = ENV.fetch('GOVUK_ONE_LOGIN_CLIENT_ID', '') + onelogin_issuer_uri = URI(ENV.fetch('GOVUK_ONE_LOGIN_ISSUER_URL', '')) + private_key_pem = ENV.fetch('GOVUK_ONE_LOGIN_PRIVATE_KEY', '') + application_url = HostingEnvironment.application_url + + begin + private_key_pem = private_key_pem.gsub('\n', "\n") + private_key = OpenSSL::PKey::RSA.new(private_key_pem) + rescue OpenSSL::PKey::RSAError => e + Rails.logger.warn "GOVUK ONE LOGIN PRIVATE error, is the key present? #{e.message}" + end + + builder.provider :govuk_one_login, + name: :onelogin, + allow_authorize_params: %i[session_id], + callback_path: '/auth/onelogin/callback', + discovery: true, + issuer: onelogin_issuer_uri.to_s, + path_prefix: '/auth', + post_logout_redirect_uri: "#{application_url}/auth/onelogin/sign-out-complete", + response_type: :code, + scope: %w[email openid], + client_auth_method: :jwt_bearer, + client_options: { + authorization_endpoint: '/oauth2/authorize', + end_session_endpoint: '/oauth2/logout', + token_endpoint: '/oauth2/token', + userinfo_endpoint: '/oauth2/userinfo', + host: onelogin_issuer_uri.host, + identifier: client_id, + port: 443, + redirect_uri: "#{application_url}/auth/onelogin/callback", + scheme: 'https', + private_key: private_key, + } + end +end diff --git a/lib/omniauth/strategies/one_login_developer.rb b/lib/omniauth/strategies/one_login_developer.rb new file mode 100644 index 00000000000..65f6a4c9261 --- /dev/null +++ b/lib/omniauth/strategies/one_login_developer.rb @@ -0,0 +1,9 @@ +require 'omniauth' + +module OmniAuth + module Strategies + class OneLoginDeveloper < Developer + include OmniAuth::Strategy + end + end +end diff --git a/spec/factories/one_login_auth.rb b/spec/factories/one_login_auth.rb new file mode 100644 index 00000000000..7cce91bf8b2 --- /dev/null +++ b/spec/factories/one_login_auth.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :one_login_auth do + candidate + token { SecureRandom.hex(10) } + email_address { "#{SecureRandom.hex(5)}@example.com" } + end +end diff --git a/spec/models/one_login_user_bypass_spec.rb b/spec/models/one_login_user_bypass_spec.rb new file mode 100644 index 00000000000..8acc625c969 --- /dev/null +++ b/spec/models/one_login_user_bypass_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe OneLoginUserBypass do + describe 'validations' do + subject { described_class.new(token: 'token') } + + it { is_expected.to validate_presence_of :token } + end + + describe 'authentificate' do + subject(:authentificate) { described_class.new(token: 'token').authentificate } + + it 'returns the candidate with existing token' do + candidate = create(:candidate) + create(:one_login_auth, candidate:, token: 'token') + + expect { authentificate }.to not_change( + candidate.reload.one_login_auth, + :id, + ) + + expect(authentificate).to eq(candidate) + end + + it 'creates a with one login auth if candidate does not exist' do + expect { authentificate }.to change( + Candidate, + :count, + ).by(1) + + expect(authentificate).to eq(Candidate.last) + expect(authentificate.one_login_auth).to have_attributes( + email_address: authentificate.email_address, + token: 'token', + ) + end + end +end diff --git a/spec/models/one_login_user_spec.rb b/spec/models/one_login_user_spec.rb new file mode 100644 index 00000000000..57ec55476d0 --- /dev/null +++ b/spec/models/one_login_user_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe OneLoginUser do + subject(:authentificate) { one_login_user.authentificate } + + let(:one_login_user) { described_class.new(omniauth_object) } + let(:omniauth_object) do + OmniAuth::AuthHash.new( + { + uid: '123', + info: { + email: 'test@email.com', + }, + }, + ) + end + + describe 'authentificate' do + it 'authentificates successfuly using the token' do + candidate = create(:candidate) + create(:one_login_auth, candidate:, token: '123') + + expect { authentificate }.to not_change( + candidate.reload.one_login_auth, + :id, + ) + + expect(authentificate).to eq(candidate) + end + + it 'authentificates successfuly using the email and creates a one_login_auth' do + candidate = create(:candidate, email_address: 'test@email.com') + + expect { authentificate }.to change { + candidate.reload.one_login_auth.present? + }.from(false).to(true) + + expect(authentificate).to eq(candidate) + expect(authentificate.one_login_auth).to have_attributes( + email_address: authentificate.email_address, + token: '123', + ) + end + + it "authentificates successfuly and creates a candidate if we can't find one" do + expect { authentificate }.to change( + Candidate, + :count, + ).by(1) + + expect(authentificate).to eq(Candidate.last) + expect(authentificate.one_login_auth).to have_attributes( + email_address: authentificate.email_address, + token: '123', + ) + end + + it 'raises error if the candidate already has a different one login token' do + candidate = create(:candidate, email_address: 'test@email.com') + create(:one_login_auth, candidate:, token: '456') + + expect { authentificate }.to raise_exception(OneLoginUser::Error).with_message( + "Candidate #{candidate.id} has a different one login " \ + 'token than the user trying to login. Token used to auth 123', + ) + end + end +end diff --git a/spec/requests/one_login_controller_spec.rb b/spec/requests/one_login_controller_spec.rb new file mode 100644 index 00000000000..9bf0fc2f5dd --- /dev/null +++ b/spec/requests/one_login_controller_spec.rb @@ -0,0 +1,210 @@ +require 'rails_helper' + +RSpec.describe 'OneLoginController' do + before do + FeatureFlag.activate(:one_login_candidate_sign_in) + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:onelogin] = omniauth_hash + end + + let(:omniauth_hash) do + OmniAuth::AuthHash.new( + { + provider: 'onelogin', + uid: '123', + info: { + email: 'test@email.com', + }, + credentials: { + id_token: 'id_token', + }, + }, + ) + end + + describe 'GET /auth/onelogin/callback' do + it 'redirects to candidate_interface_interstitial_path' do + candidate = create(:candidate) + create(:one_login_auth, candidate:, token: '123') + + get auth_onelogin_callback_path + + expect(response).to redirect_to(candidate_interface_interstitial_path) + end + + context 'when there is no omniauth_hash' do + let(:omniauth_hash) { nil } + + it 'returns unprocessable_entity' do + get auth_onelogin_callback_path + + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'when candidate has a different onelogin token than the one returned by onelogin' do + it 'redirects to auth_onelogin_sign_out_path' do + candidate = create(:candidate, email_address: 'test@email.com') + create(:one_login_auth, candidate:, token: '456') + + get auth_onelogin_callback_path + + expect(response).to redirect_to(auth_onelogin_sign_out_path) + expect(session[:one_login_error]).to eq( + "Candidate #{candidate.id} has a different one login token than the " \ + 'user trying to login. Token used to auth 123', + ) + end + end + end + + describe 'GET /auth/one-login-developer/callback' do + before do + Rails.application.env_config['omniauth.auth'] = omniauth_one_login_developer + end + + let(:omniauth_one_login_developer) do + OmniAuth.config.mock_auth[:one_login_developer] = omniauth_developer_hash + end + let(:omniauth_developer_hash) do + OmniAuth::AuthHash.new( + { + provider: :one_login_developer, + uid:, + credentials: { + id_token: 'id_token', + }, + }, + ) + end + let(:uid) { '123' } + + it 'logs in the candidate' do + candidate = create(:candidate) + create(:one_login_auth, candidate:, token: '123') + + get auth_one_login_developer_callback_path + + expect(response).to redirect_to(candidate_interface_interstitial_path) + end + + context 'when candidate does not exist in DB' do + it 'creates a candidate and logs them in' do + expect { get auth_one_login_developer_callback_path }.to change( + Candidate, + :count, + ).by(1) + expect(response).to redirect_to(candidate_interface_interstitial_path) + end + end + + context 'when uid is blank' do + let(:uid) { nil } + + it 'redirects to sign in page' do + candidate = create(:candidate) + create(:one_login_auth, candidate:, token: '123') + + get auth_one_login_developer_callback_path + + expect(response).to redirect_to(candidate_interface_create_account_or_sign_in_path) + end + end + end + + describe 'GET /auth/onelogin/sign_out' do + it 'redirects to one_login logout url' do + create(:candidate, email_address: 'test@email.com') + + get auth_onelogin_callback_path # set the session variables + get auth_onelogin_sign_out_path + + params = { + post_logout_redirect_uri: URI(auth_onelogin_sign_out_complete_url), + id_token_hint: 'id_token', + } + one_login_logout_url = URI.parse("#{ENV['GOVUK_ONE_LOGIN_ISSUER_URL']}logout").tap do |uri| + uri.query = URI.encode_www_form(params) + end.to_s + + expect(response).to redirect_to(one_login_logout_url) + end + + context 'when candidate has a different onelogin token than the one returned by onelogin' do + it 'redirects to one_login logout url and persists the session error message' do + candidate = create(:candidate, email_address: 'test@email.com') + create(:one_login_auth, candidate:, token: '456') + + get auth_onelogin_callback_path # set the session variables + get auth_onelogin_sign_out_path + + expect(session[:onelogin_id_token]).to be_nil + expect(session[:one_login_error]).to eq( + "Candidate #{candidate.id} has a different one login token than the " \ + 'user trying to login. Token used to auth 123', + ) + + params = { + post_logout_redirect_uri: URI(auth_onelogin_sign_out_complete_url), + id_token_hint: 'id_token', + } + one_login_url = URI.parse("#{ENV['GOVUK_ONE_LOGIN_ISSUER_URL']}logout").tap do |uri| + uri.query = URI.encode_www_form(params) + end.to_s + + expect(response).to redirect_to(one_login_url) + end + end + + context 'when one login bypass is true' do + it 'redirects to sign_in page' do + allow(OneLogin).to receive(:bypass?).and_return(true) + + get auth_onelogin_sign_out_path + expect(response).to redirect_to candidate_interface_create_account_or_sign_in_path + end + end + end + + describe 'GET /auth/onelogin/sign_out_complete' do + context 'when candidate has a different onelogin token than the one returned by onelogin' do + it 'redirects to logout_onelogin_path and persists the session error message' do + candidate = create(:candidate, email_address: 'test@email.com') + create(:one_login_auth, candidate:, token: '456') + allow(Sentry).to receive(:capture_message) + + get auth_onelogin_callback_path # set the session variables + get auth_onelogin_sign_out_complete_path + + expect(Sentry).to have_received(:capture_message).with( + "Candidate #{candidate.id} has a different one login token than the " \ + 'user trying to login. Token used to auth 123', + level: :error, + ) + expect(response).to redirect_to(internal_server_error_path) + end + end + + context 'candidate has no errors' do + it 'redirects to logout_onelogin_path and persists the session error message' do + get auth_onelogin_sign_out_complete_path + + expect(response).to redirect_to( + candidate_interface_create_account_or_sign_in_path, + ) + end + end + end + + describe 'GET /auth/onelogin/failure' do + it 'redirects to auth_failure_path with one login error' do + get auth_onelogin_callback_path # set the session variables + get auth_failure_path(params: { message: 'error_message' }) + + expect(session[:one_login_error]).to eq( + 'One login failure with error_message for onelogin_id_token: id_token', + ) + expect(response).to redirect_to(auth_onelogin_sign_out_path) + end + end +end diff --git a/spec/support/test_helpers/one_login_helper.rb b/spec/support/test_helpers/one_login_helper.rb new file mode 100644 index 00000000000..9eb796f40a2 --- /dev/null +++ b/spec/support/test_helpers/one_login_helper.rb @@ -0,0 +1,16 @@ +module OneLoginHelper + def user_exists_in_onelogin(email_address: 'test@email.com', uid: 'UID') + OmniAuth.config.mock_auth[:onelogin] = OmniAuth::AuthHash.new( + { + provider: 'onelogin', + uid:, + info: { + email: email_address, + }, + credentials: { + id_token: 'id_token', + }, + }, + ) + end +end diff --git a/spec/system/candidate_interface/one_login_signup/candidate_signs_in_spec.rb b/spec/system/candidate_interface/one_login_signup/candidate_signs_in_spec.rb new file mode 100644 index 00000000000..b7c9bd14808 --- /dev/null +++ b/spec/system/candidate_interface/one_login_signup/candidate_signs_in_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +RSpec.describe 'Candidate signs in' do + include OneLoginHelper + + scenario 'Candidate signs in and signs out' do + given_i_have_a_candidate_record + given_i_have_one_login_account(@candidate.email_address) + given_the_one_login_feature_flag_is_active + + when_i_visit_the_candidate_application_path + then_i_am_redirected_to_the_candidate_sign_in_path + when_i_click_continue + then_i_am_redirected_to_the_candidate_application_path + + when_i_click_sign_out + i_am_redirected_back_to_sign_in_page + + when_i_visit_the_candidate_application_path + then_i_am_redirected_to_the_candidate_sign_in_path + end + + scenario 'Candidate signs in without a candidate account' do + given_i_have_one_login_account('test@email.com') + given_the_one_login_feature_flag_is_active + + when_i_visit_the_candidate_application_path + then_i_am_redirected_to_the_candidate_sign_in_path + when_i_click_continue + end + + scenario 'Candidate already has a different one login attached to candidate record' do + given_i_have_a_candidate_record + given_i_already_have_a_different_one_login_token(@candidate) + given_i_have_one_login_account(@candidate.email_address) + given_the_one_login_feature_flag_is_active + + when_i_visit_the_candidate_application_path + then_i_am_redirected_to_the_candidate_sign_in_path + when_i_click_continue + i_am_redirected_back_to_sign_in_page + end + + def given_i_have_a_candidate_record + @candidate = create(:candidate) + end + + def given_i_have_one_login_account(email_address) + user_exists_in_onelogin(email_address:) + end + + def given_i_already_have_a_different_one_login_token(candidate) + candidate.create_one_login_auth!( + token: '123', + email_address: candidate.email_address, + ) + end + + def given_the_one_login_feature_flag_is_active + FeatureFlag.activate(:one_login_candidate_sign_in) + end + + def when_i_visit_the_candidate_application_path + visit candidate_interface_details_path + end + + def then_i_am_redirected_to_the_candidate_sign_in_path + expect(page).to have_current_path( + candidate_interface_create_account_or_sign_in_path, + ) + end + + def when_i_click_continue + click_link_or_button 'Continue' + end + + def then_i_am_redirected_to_the_candidate_application_path + expect(page).to have_current_path( + candidate_interface_details_path, + ) + end + + def when_i_click_sign_out + click_link_or_button 'Sign out' + end + + def i_am_redirected_back_to_sign_in_page + expect(page).to have_current_path( + '/candidate/account', + ) + end +end