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 @@
+ <%= 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 %> + +- 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 %>