Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,8 @@ def default_locale?(locale)

false
end

def auth_service
@auth_service ||= AuthService.new(session)
end
end
7 changes: 6 additions & 1 deletion app/controllers/forms/check_your_answers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ def submit_answers

current_context.save_submission_details(submission_reference, requested_email_confirmation)

redirect_to :form_submitted
if auth_service.logged_in?
auth_service.store_return_params(form: current_context.form, mode: mode, locale: locale_param)
redirect_to auth_service.logout_redirect_uri(omniauth_logged_out_url), allow_other_host: true
else
redirect_to :form_submitted
end
rescue FormSubmissionService::ConfirmationEmailToAddressError
setup_check_your_answers
email_confirmation_input.errors.add(:confirmation_email_address, :invalid_email)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/forms/continue_to_one_login_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class ContinueToOneLoginController < BaseController
before_action :redirect_if_feature_disabled, :redirect_if_form_incomplete

def show
Store::ReturnFromOneLoginStore.new(session).store_return_params(
auth_service.store_return_params(
form: current_context.form,
mode: mode,
locale: locale_param,
Expand Down
28 changes: 13 additions & 15 deletions app/controllers/users/omniauth_controller.rb
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
module Users
class OmniauthController < ApplicationController
class OmniAuthLoggedInDataMissingError < StandardError; end
class OmniAuthFailure < StandardError; end

rescue_from Store::ReturnFromOneLoginStore::MissingReturnParamsError do
Rails.logger.warn("Missing return params in session for One Login callback")
redirect_to error_404_path
end

def callback
email = request.env.dig("omniauth.auth", "info", "email")
if email.blank?
raise OmniAuthLoggedInDataMissingError, "Email is missing in OmniAuth auth hash"
end

return_from_one_login_store = Store::ReturnFromOneLoginStore.new(session)
form_id = return_from_one_login_store.form_id
path_params = return_from_one_login_store.get_path_params
auth_hash = request.env["omniauth.auth"]
auth_service.store_auth_details(auth_hash)

Store::ConfirmationDetailsStore.new(session, form_id).save_copy_of_answers_email_address(email)
path_params = auth_service.form_path_params

redirect_to check_your_answers_path(**path_params)
rescue Store::ReturnFromOneLoginStore::MissingReturnParamsError
Rails.logger.warn("Missing return params in session for One Login callback")
redirect_to error_404_path
end

def failure
error = request.env["omniauth.error"]
raise OmniAuthFailure, error
end

def logged_out
auth_service.clear_auth_session

path_params = auth_service.form_path_params
redirect_to form_submitted_path(**path_params)
end
end
end
28 changes: 28 additions & 0 deletions app/lib/store/auth_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Store
class AuthStore
AUTH_KEY = "auth".freeze
TOKEN_KEY = "token".freeze

def initialize(store)
@store = store
end

def store_token(token)
@store[AUTH_KEY] = {
TOKEN_KEY => token,
}
end

def get_token
@store.dig(AUTH_KEY, TOKEN_KEY)
end

def clear
@store.delete(AUTH_KEY)
end

def logged_in?
get_token.present?
end
end
end
2 changes: 1 addition & 1 deletion app/lib/store/return_from_one_login_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def store_return_params(form:, mode:, locale:)
@store[RETURN_FROM_ONE_LOGIN_KEY][LAST_LOCALE_KEY] = locale
end

def get_path_params
def form_path_params
raise MissingReturnParamsError if @store[RETURN_FROM_ONE_LOGIN_KEY].blank?

{
Expand Down
56 changes: 56 additions & 0 deletions app/services/auth_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class AuthService
class DataMissingError < StandardError; end

attr_reader :auth_store, :return_from_one_login_store

def initialize(store)
@store = store
@return_from_one_login_store = Store::ReturnFromOneLoginStore.new(store)
@auth_store = Store::AuthStore.new(store)
end

delegate :logged_in?, to: :auth_store
delegate :store_return_params, :form_path_params, to: :return_from_one_login_store

def store_auth_details(auth_hash)
form_id = @return_from_one_login_store.form_id

raise DataMissingError, "Auth hash is missing on request" if auth_hash.blank?

email = auth_hash.dig("info", "email")
raise DataMissingError, "Email is missing in OmniAuth auth hash" if email.blank?

token = auth_hash.dig("credentials", "id_token")
raise DataMissingError, "Token is missing in OmniAuth auth hash" if token.blank?

@auth_store.store_token(token)
Store::ConfirmationDetailsStore.new(@store, form_id).save_copy_of_answers_email_address(email)
end

def logout_redirect_uri(post_logout_redirect_uri)
token = @auth_store.get_token
logout_request = logout_utility.build_request(
id_token_hint: token,
post_logout_redirect_uri: url_without_params(post_logout_redirect_uri),
)
logout_request.redirect_uri
end

def clear_auth_session
@auth_store.clear
end

private

def logout_utility
@logout_utility ||= OmniAuth::GovukOneLogin::LogoutUtility.new(
idp_configuration: Rails.application.config.x.one_login.idp_configuration,
)
end

def url_without_params(url)
url = URI.parse(url)
url.query = nil
url.to_s
end
end
3 changes: 3 additions & 0 deletions config/initializers/omniauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@
# will call `Users::OmniauthController#failure` if there are any errors during the login process
on_failure { |env| Users::OmniauthController.action(:failure).call(env) }
end

# Store this globally so we only make a request to the One Login discovery endpoint once as the configuration should not regularly change
Rails.application.config.x.one_login.idp_configuration = OmniAuth::GovukOneLogin::IdpConfiguration.new(idp_base_url: Settings.govuk_one_login.base_url)
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

get "/auth/govuk_one_login/callback", to: "users/omniauth#callback", as: :omniauth_callback
get "/auth/failure", to: "users/omniauth#failure", as: :omniauth_failure
get "/auth/logged-out", to: "users/omniauth#logged_out", as: :omniauth_logged_out

# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
Expand Down
3 changes: 3 additions & 0 deletions config/settings/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ submission_status_api:
file_upload:
poll_scan_status_wait_milliseconds: 50
poll_scan_status_max_attempts: 2

govuk_one_login:
base_url: http://example.com/one-login-mock/
26 changes: 26 additions & 0 deletions spec/lib/store/auth_store_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "rails_helper"

RSpec.describe Store::AuthStore do
subject(:auth_store) { described_class.new(store) }

let(:store) { {} }
let(:token) { Faker::Alphanumeric.alphanumeric }

it "stores and returns the auth token" do
auth_store.store_token(token)

expect(auth_store.get_token).to eq(token)
end

describe "#logged_in" do
it "returns true when the auth token is stored" do
auth_store.store_token(token)

expect(auth_store.logged_in?).to be true
end

it "returns false when the auth token is not stored" do
expect(auth_store.logged_in?).to be false
end
end
end
4 changes: 2 additions & 2 deletions spec/lib/store/return_from_one_login_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
it "stores and return the path params" do
return_from_one_login_store.store_return_params(form:, mode:, locale:)

expect(return_from_one_login_store.get_path_params).to eq({
expect(return_from_one_login_store.form_path_params).to eq({
mode: mode.to_s,
form_id: form.id,
form_slug: form.form_slug,
Expand All @@ -21,7 +21,7 @@
end

it "raises an error if the return params have not been stored" do
expect { return_from_one_login_store.get_path_params }.to raise_error(Store::ReturnFromOneLoginStore::MissingReturnParamsError)
expect { return_from_one_login_store.form_path_params }.to raise_error(Store::ReturnFromOneLoginStore::MissingReturnParamsError)
end
end

Expand Down
65 changes: 52 additions & 13 deletions spec/requests/forms/check_your_answers_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,23 @@

let(:submission_email) { Faker::Internet.email(domain: "example.gov.uk") }

let(:store) do
let(:answers) do
{
answers: {
form_id.to_s => {
"1" => {
"date_year" => "2000",
"date_month" => "1",
"date_day" => "1",
},
"2" => {
"date_year" => "2023",
"date_month" => "6",
"date_day" => "9",
},
form_id.to_s => {
"1" => {
"date_year" => "2000",
"date_month" => "1",
"date_day" => "1",
},
"2" => {
"date_year" => "2023",
"date_month" => "6",
"date_day" => "9",
},
},
}
end
let(:store) { { answers: }.with_indifferent_access }

let(:steps_data) do
[
Expand Down Expand Up @@ -93,6 +92,9 @@
allow(context_spy).to receive(:form_submitted?).and_return(repeat_form_submission)
context_spy
end
allow(Store::AuthStore).to receive(:new).and_wrap_original do |original_method, *_args|
original_method.call(store)
end

allow(ReferenceNumberService).to receive(:generate).and_return(reference)
allow(FeatureService).to receive(:enabled?).with("filler_answer_email_enabled").and_return(true)
Expand Down Expand Up @@ -314,6 +316,43 @@
end
end

context "when the user has logged in with One Login" do
let(:token) { Faker::Alphanumeric.alphanumeric }
let(:store) do
{
answers:,
auth: { token: },
}.with_indifferent_access
end
let(:end_session_endpoint) { "http://example.com/one-login-mock/logout" }

before do
allow(AuthService).to receive(:new).and_wrap_original do |original_method, *_args|
original_method.call(store)
end

idp_configuration = instance_double(OmniAuth::GovukOneLogin::IdpConfiguration, end_session_endpoint:)
allow(Rails.application.config.x).to receive(:one_login).and_return(double(idp_configuration:))

post form_submit_answers_path(form_id:, form_slug: "form-name", mode:), params: { email_confirmation_input: }
end

it "saves the path params for returning from One Login on the session" do
expect(store).to have_key "return_from_one_login"
expect(store["return_from_one_login"]).to eq({
"last_form_id" => form_data.form_id,
"last_form_slug" => form_data.form_slug,
"last_mode" => mode.to_s,
"last_locale" => nil,
})
end

it "redirects to the One Login logout page" do
post_logout_redirect_url = CGI.escape("http://www.example.com/auth/logged-out")
expect(response).to redirect_to(/#{end_session_endpoint}\?id_token_hint=#{token}&post_logout_redirect_uri=#{post_logout_redirect_url}/)
end
end

context "when answers have already been submitted" do
let(:repeat_form_submission) { true }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
allow(Flow::Context).to receive(:new).and_wrap_original do |original_method, *args|
original_method.call(form: args[0][:form], form_document: args[0][:form_document], store:)
end
allow(Store::ReturnFromOneLoginStore).to receive(:new).and_wrap_original do |original_method, *_args|
allow(AuthService).to receive(:new).and_wrap_original do |original_method, *_args|
original_method.call(store)
end
end
Expand Down
Loading