From ba2c3052fe258faac8c5c0f312d83d0c0dc684a9 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 20 Oct 2023 16:17:21 +0100 Subject: [PATCH 001/122] wip --- .gitignore | 4 ++ Gemfile | 3 + Gemfile.lock | 61 +++++++++++++++++ .../users/omniauth_callbacks_controller.rb | 23 +++++++ app/helpers/application_helper.rb | 8 +-- app/helpers/gov_one_helper.rb | 11 ++++ app/models/user.rb | 10 ++- app/services/gov_one_auth_service.rb | 65 +++++++++++++++++++ app/views/home/index.html.slim | 6 +- app/views/user/show.html.slim | 8 --- config/initializers/devise.rb | 19 +++++- config/routes.rb | 3 + .../omniauth_callbacks_controller_spec.rb | 48 ++++++++++++++ .../users/sessions_controller_spec.rb | 49 +++++++------- spec/rails_helper.rb | 2 + spec/services/gov_one_auth_service_spec.rb | 52 +++++++++++++++ 16 files changed, 328 insertions(+), 44 deletions(-) create mode 100644 app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 app/helpers/gov_one_helper.rb create mode 100644 app/services/gov_one_auth_service.rb create mode 100644 spec/controllers/users/omniauth_callbacks_controller_spec.rb create mode 100644 spec/services/gov_one_auth_service_spec.rb diff --git a/.gitignore b/.gitignore index 8ca8af16b..ba33dc4ad 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,10 @@ localhost.crt localhost.key nscacert.pem +# gov one login keys +private_key.pem +public_key.pem + # Yarn files .yarn/* diff --git a/Gemfile b/Gemfile index d37479bdd..d6a013d5b 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,9 @@ gem 'bootsnap', require: false # User authentication gem 'devise' +gem 'jwt' +gem 'omniauth_openid_connect' +gem 'omniauth-rails_csrf_protection' # HTML abstraction markup language gem 'slim-rails' diff --git a/Gemfile.lock b/Gemfile.lock index e20b78f6b..e3f0b4985 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,11 +68,13 @@ GEM tzinfo (~> 2.0) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) + aes_key_wrap (1.1.0) ahoy_matey (5.0.1) activesupport (>= 6.1) device_detector (>= 1) safely_block (>= 0.4) ast (2.4.2) + attr_required (1.0.1) backports (3.24.1) base64 (0.1.1) bcrypt (3.1.19) @@ -80,6 +82,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) + bindata (2.4.15) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -180,6 +183,8 @@ GEM base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) faraday-net_http (3.0.2) ffi (1.16.3) ffi-compiler (1.0.1) @@ -310,6 +315,12 @@ GEM jsbundling-rails (1.2.1) railties (>= 6.0.0) json (2.6.3) + json-jwt (1.16.3) + activesupport (>= 4.2) + aes_key_wrap + bindata + faraday (~> 2.0) + faraday-follow_redirects jwt (2.7.1) kramdown (2.4.0) rexml @@ -358,6 +369,29 @@ GEM racc (~> 1.4) notifications-ruby-client (5.4.0) jwt (>= 1.5, < 3) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + omniauth_openid_connect (0.7.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.2.0) + activemodel + attr_required (>= 1.0.0) + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + net-smtp + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_email + validate_url + webfinger (~> 2.0) opentelemetry-api (1.2.3) opentelemetry-common (0.20.0) opentelemetry-api (~> 1.0) @@ -588,6 +622,15 @@ GEM raabro (1.4.0) racc (1.7.1) rack (2.2.8) + rack-oauth2 (2.2.0) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) rack-proxy (0.7.7) rack rack-test (2.1.0) @@ -758,6 +801,11 @@ GEM stimulus-rails (1.2.2) railties (>= 6.0.0) stringio (3.0.8) + swd (2.0.2) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects temple (0.10.3) thor (1.2.2) tilt (2.3.0) @@ -774,6 +822,12 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.5.0) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix view_component (3.5.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) @@ -785,6 +839,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webfinger (2.1.2) + activesupport + faraday (~> 2.0) + faraday-follow_redirects webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -841,7 +899,10 @@ DEPENDENCIES importmap-rails jbuilder jsbundling-rails + jwt launchy + omniauth-rails_csrf_protection + omniauth_openid_connect pg pry-byebug pry-doc diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..893603078 --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,23 @@ +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + def openid_connect + code = params['code'] + + auth_service = GovOneAuthService.new(code) + tokens_response = auth_service.tokens + access_token = tokens_response['access_token'] + session[:id_token] = tokens_response['id_token'] + + user_info_response = auth_service.user_info(access_token) + @email = user_info_response['email'] + + gov_user = find_or_create_user + sign_in_and_redirect gov_user if gov_user + end + +private + + # @return [User] + def find_or_create_user + User.find_by(email: @email) || create_from_email(@email) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0ff6ce268..baa259d88 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,14 +5,14 @@ def navigation header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) if user_signed_in? header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) - header.with_action_link(text: 'Sign out', href: destroy_user_session_path, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) + header.with_action_link(text: 'Sign out', href: logout_uri, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? header.with_navigation_item(text: 'My account', href: user_path, classes: ['dfe-header__navigation-item dfe-header-f-mob']) - header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }, classes: ['dfe-header__navigation-item dfe-header-f-mob'], html_attributes: { id: 'sign-out-f-mob' }) + header.with_navigation_item(text: 'Sign out', href: logout_uri, options: { data: { turbo_method: :get } }, classes: ['dfe-header__navigation-item dfe-header-f-mob'], html_attributes: { id: 'sign-out-f-mob' }) else - header.with_action_link(text: 'Sign in', href: new_user_session_path, options: { inverse: true }) - header.with_navigation_item(text: 'Sign in', href: new_user_session_path, classes: ['dfe-header__navigation-item dfe-header-f-mob']) + header.with_action_link(text: 'Sign in', href: login_uri, options: { inverse: true }) + header.with_navigation_item(text: 'Sign in', href: login_uri, classes: ['dfe-header__navigation-item dfe-header-f-mob']) end end end diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb new file mode 100644 index 000000000..a8eefb2dc --- /dev/null +++ b/app/helpers/gov_one_helper.rb @@ -0,0 +1,11 @@ +module GovOneHelper + # @return [String] + def login_uri + "#{ENV['GOV_ONE_AUTH_URI']}&nonce=#{SecureRandom.uuid}&state=#{SecureRandom.uuid}" + end + + # @return [String] + def logout_uri + "#{ENV['GOV_ONE_SIGN_OUT_URI']}?id_token_hint=#{session[:id_token]}&post_logout_redirect_uri=#{ENV['GOV_ONE_SIGN_OUT_REDIRECT_URI']}&state=#{SecureRandom.uuid}" + end +end diff --git a/app/models/user.rb b/app/models/user.rb index c473bb527..2cbec1b77 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,12 +21,20 @@ def self.dashboard_headers DASHBOARD_ATTRS + Training::Module.live.map { |mod| "module_#{mod.position}_time" } end + # @return [User] + def self.create_from_email(email) + user = User.new(email: email, confirmed_at: Time.zone.now) + user.save(validate: false) + user + end + # Include default devise modules. Others available are: # :timeoutable, :trackable, :recoverable and :omniauthable attr_accessor :context devise :database_authenticatable, :registerable, :recoverable, - :validatable, :rememberable, :confirmable, :lockable, :timeoutable + :validatable, :rememberable, :confirmable, :lockable, :timeoutable, + :omniauthable, omniauth_providers: [:openid_connect] has_many :responses has_many :user_answers diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb new file mode 100644 index 000000000..ff86c9765 --- /dev/null +++ b/app/services/gov_one_auth_service.rb @@ -0,0 +1,65 @@ +class GovOneAuthService + # @param code [String] + def initialize(code) + @code = code + end + + # @return [Hash] + def tokens + body = { + grant_type: 'authorization_code', + code: @code, + redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], + client_assertion_type: ENV['GOV_ONE_CLIENT_ASSERTION_TYPE'], + client_assertion: jwt_assertion, + } + + token_uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") + http = build_http(token_uri) + + token_request = Net::HTTP::Post.new(token_uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) + token_request.set_form_data(body) + token_response = http.request(token_request) + + JSON.parse(token_response.body) + end + + # @param access_token [String] + # @return [Hash] + def user_info(access_token) + userinfo_uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/userinfo") + http = build_http(userinfo_uri) + + userinfo_request = Net::HTTP::Get.new(userinfo_uri.path, { 'Authorization' => "Bearer #{access_token}" }) + userinfo_response = http.request(userinfo_request) + + JSON.parse(userinfo_response.body) + end + +private + + # @param uri [URI] + # @return [Net::HTTP] + def build_http(uri) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http + end + + # @return [String] + def jwt_assertion + rsa_public = OpenSSL::PKey::RSA.new(File.read('public_key.pem')) + rsa_private = OpenSSL::PKey::RSA.new(File.read('private_key.pem')) + + payload = { + aud: "#{ENV['GOV_ONE_BASE_URI']}/token", + iss: ENV['GOV_ONE_CLIENT_ID'], + sub: ENV['GOV_ONE_CLIENT_ID'], + exp: Time.now.to_i + 5 * 60, + jti: SecureRandom.uuid, + iat: Time.now.to_i, + } + + JWT.encode payload, rsa_private, 'RS256' + end +end diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index f7bc8011c..b9a694e94 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -13,7 +13,7 @@ .govuk-grid-row .govuk-grid-column-two-thirds = content_resource 'home.hero', service_name: service_name - + = govuk_button_link_to course_overview_path, class: 'govuk-button--start govuk-!-margin-bottom-4' do - if current_user | Learn more @@ -37,6 +37,4 @@ = content_resource 'home.login' .govuk-button-group - = govuk_button_link_to 'Sign in', new_user_session_path - .white-space-pre-wrap= ' or ' - = govuk_link_to 'create an account', new_user_registration_path + = govuk_button_link_to 'Sign in with Gov One Login', login_uri diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 6febd1cfe..8ec8ad705 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -14,14 +14,6 @@ - row.with_key { 'Name' } - row.with_value(text: current_user.name, classes: ['data-hj-suppress']) - row.with_action(text: t('links.change'), href: edit_registration_name_path, visually_hidden_text: 'name', html_attributes: { id: :edit_name_registration }) - - your_details.with_row do |row| - - row.with_key { 'Email' } - - row.with_value(text:current_user.email, classes: ['data-hj-suppress']) - - row.with_action(text: t('links.change'), href: edit_email_user_path, visually_hidden_text: 'email', html_attributes: { id: :edit_email_user }) - - your_details.with_row do |row| - - row.with_key { 'Password' } - - row.with_value { t('my_account.password_changed', date: current_user.password_last_changed) } - - row.with_action(text: t('links.change'), href: edit_password_user_path, visually_hidden_text: 'password', html_attributes: { id: :edit_password_user }) = govuk_summary_list do |other_details| - other_details.with_row do |row| diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f7ca631b4..cfbf68e79 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -306,9 +306,22 @@ def i18n_message(default = nil) config.sign_out_via = :get # ==> OmniAuth - # Add a new OmniAuth provider. Check the wiki for more information on setting - # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :openid_connect, { + name: :gov_one, + scope: %i[openid email], + response_type: :code, + client_options: { + port: 443, + scheme: 'https', + host: 'oidc.integration.account.gov.uk', + identifier: ENV['GOV_ONE_CLIENT_ID'], + secret: ENV['GOV_ONE_CLIENT_SECRET'], + redirect_uri: 'users/auth/openid_connect/callback', + }, + authorize_params: { + prompt: 'login', + }, + } # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/routes.rb b/config/routes.rb index e149d70fe..d1b01e2e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,7 @@ confirmations: 'confirmations', passwords: 'passwords', registrations: 'registrations', + omniauth_callbacks: 'users/omniauth_callbacks', }, path_names: { sign_in: 'sign-in', @@ -31,6 +32,7 @@ get 'check_session_timeout', to: 'timeout#check' get 'extend_session', to: 'timeout#extend' get 'users/timeout', to: 'timeout#timeout_user' + get '/users/sign_out', to: 'users/sessions#destroy' end namespace :registration do @@ -52,6 +54,7 @@ get 'check-email-confirmation' get 'check-email-password-reset' get 'edit-training-emails' + devise_for :users, controllers: { omniauth_callbacks: 'controllers/users/omniauth_callbacks' } patch 'update-training-emails' resource :close_account, only: %i[new update show], path: 'close' do diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb new file mode 100644 index 000000000..85875a3a9 --- /dev/null +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +RSpec.describe Users::OmniauthCallbacksController, type: :controller do + + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + end + describe '#openid_connect' do + let(:auth_service) { instance_double(GovOneAuthService) } + let(:access_token) { 'mock_access_token' } + let(:id_token) { 'mock_id_token' } + let(:email) { 'test@example.com' } + + before do + allow(GovOneAuthService).to receive(:new).and_return(auth_service) + allow(auth_service).to receive(:tokens).and_return('access_token' => access_token, 'id_token' => id_token) + allow(auth_service).to receive(:user_info).with(access_token).and_return('email' => email) + end + + it 'sets session id_token and finds or creates the user' do + get :openid_connect, params: { 'code' => 'mock_code' } + + expect(session[:id_token]).to eq(id_token) + expect(assigns(:email)).to eq(email) + expect(response).to redirect_to(root_path) + end + + it 'signs in and redirects the user if found or created' do + user = create(:user, email: email) # Assuming you have a User model and FactoryBot + + allow(controller).to receive(:find_or_create_user).and_return(user) + + get :openid_connect, params: { 'code' => 'mock_code' } + + expect(response).to redirect_to(root_path) # Adjust the path based on your actual redirect path + expect(subject.current_user).to eq(user) + end + + it 'does not sign in if user is not found or created' do + allow(controller).to receive(:find_or_create_user).and_return(nil) + + get :openid_connect, params: { 'code' => 'mock_code' } + + expect(response).to redirect_to(root_path) # Adjust the path based on your actual redirect path + expect(subject.current_user).to be_nil + end + end +end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index da99a8ef8..efad75a26 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -1,46 +1,47 @@ require 'rails_helper' -RSpec.describe Users::SessionsController, type: :controller do +RSpec.describe Users::OmniauthCallbacksController, type: :controller do before do request.env['devise.mapping'] = Devise.mappings[:user] - post :create, params: { user: { email: user.email, password: user.password } } + get :openid_connect, params: { 'code' => 'mock_code' } end - let(:user) { create :user, :registered } + let(:auth_service) { instance_double(GovOneAuthService) } + let(:access_token) { 'mock_access_token' } + let(:id_token) { 'mock_id_token' } + let(:email) { 'test@example.com' } - context 'when registration is complete' do - it 'continues to their training overview' do - expect(response).to redirect_to '/my-modules' - end + before do + allow(GovOneAuthService).to receive(:new).and_return(auth_service) + allow(auth_service).to receive(:tokens).and_return('access_token' => access_token, 'id_token' => id_token) + allow(auth_service).to receive(:user_info).with(access_token).and_return('email' => email) end - context 'when new feature alert is enabled' do - let(:user) do - create :user, :registered, display_whats_new: true - end + context 'when user is found or created' do + let(:user) { create(:user, email: email) } - it 'redirects to /whats-new' do - expect(response).to redirect_to '/whats-new' + before do + allow(controller).to receive(:find_or_create_user).and_return(user) end - end - context 'when email preferences are not defined' do - let(:user) do - create :user, :registered, training_emails: nil + it 'sets session id_token' do + expect(session[:id_token]).to eq(id_token) end - it 'redirects to email preferences form' do - expect(response).to redirect_to '/email-preferences' + it 'signs in the user and redirects to root path' do + expect(response).to redirect_to(root_path) + expect(subject.current_user).to eq(user) end end - context 'when registered during private beta' do - let(:user) do - create :user, :registered, registration_complete: false, private_beta_registration_complete: true + context 'when user is not found or created' do + before do + allow(controller).to receive(:find_or_create_user).and_return(nil) end - it 'redirects to new registration' do - expect(response).to redirect_to '/new-registration' + it 'does not sign in and redirects to root path' do + expect(response).to redirect_to(sign_in_path) + expect(subject.current_user).to be_nil end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 26f221ef7..da2af60c4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -69,4 +69,6 @@ # enable ShowMeTheCookies config.include ShowMeTheCookies, type: :system + + config.include Devise::Test::ControllerHelpers, type: :controller end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb new file mode 100644 index 000000000..79a6af1d8 --- /dev/null +++ b/spec/services/gov_one_auth_service_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +RSpec.describe GovOneAuthService, type: :service do + let(:code) { 'mock_code' } + let(:access_token) { 'mock_access_token' } + let(:id_token) { 'mock_id_token' } + let(:user_info) { { 'email' => 'test@example.com' } } + + before do + allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('http://example.com/callback') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('urn:ietf:params:oauth:client-assertion-type:jwt-bearer') + allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('http://gov-one-example.com') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('your_client_id') + + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with('public_key.pem').and_return('mock_public_key_content') + allow(File).to receive(:read).with('private_key.pem').and_return('mock_private_key_content') + + allow(SecureRandom).to receive(:uuid).and_return('mock_uuid') + + @auth_service = described_class.new(code) + end + + describe '#tokens' do + it 'returns a hash with access_token and id_token' do + allow(@auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') + + token_response = { 'access_token' => access_token, 'id_token' => id_token } + + http_double = instance_double(Net::HTTP, request: instance_double(Net::HTTPResponse, body: token_response.to_json)) + allow(@auth_service).to receive(:build_http).and_return(http_double) + + result = @auth_service.tokens + + expect(result).to be_a(Hash) + expect(result['access_token']).to eq(access_token) + expect(result['id_token']).to eq(id_token) + end + end + + describe '#user_info' do + it 'returns user information hash' do + http_double = instance_double(Net::HTTP, request: instance_double(Net::HTTPResponse, body: user_info.to_json)) + allow(@auth_service).to receive(:build_http).and_return(http_double) + + result = @auth_service.user_info(access_token) + + expect(result).to be_a(Hash) + expect(result['email']).to eq('test@example.com') + end + end +end From 1bb01925970611b05256df02439f151b715592a7 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 24 Oct 2023 13:22:12 +0100 Subject: [PATCH 002/122] add option for gov one login --- Gemfile.lock | 8 ++ .../users/omniauth_callbacks_controller.rb | 30 +++++- app/models/user.rb | 2 +- app/services/gov_one_auth_service.rb | 13 ++- app/views/home/index.html.slim | 5 + config/application.rb | 3 + config/credentials/development.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/test.yml.enc | 2 +- config/initializers/devise.rb | 1 - .../omniauth_callbacks_controller_spec.rb | 48 +++++----- .../users/sessions_controller_spec.rb | 49 +++++----- spec/services/gov_one_auth_service_spec.rb | 93 ++++++++++--------- 13 files changed, 150 insertions(+), 108 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e3f0b4985..c7e8547b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,6 +122,8 @@ GEM contentful_model (~> 1.0) rails (>= 4.2) redcarpet (~> 3.2) + crack (0.4.5) + rexml crass (1.0.6) cssbundling-rails (1.3.3) railties (>= 6.0.0) @@ -284,6 +286,7 @@ GEM grover (1.1.5) combine_pdf (~> 1.0) nokogiri (~> 1.0) + hashdiff (1.0.1) hashie (5.0.0) html-attributes-utils (1.0.2) activesupport (>= 6.1.4.4) @@ -843,6 +846,10 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects + webmock (3.19.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -929,6 +936,7 @@ DEPENDENCIES turbo-rails tzinfo-data web-console + webmock yard-junk RUBY VERSION diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 893603078..7214ae173 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -4,12 +4,20 @@ def openid_connect auth_service = GovOneAuthService.new(code) tokens_response = auth_service.tokens + if tokens_response.empty? || tokens_response['access_token'].blank? || tokens_response['id_token'].blank? + flash[:alert] = 'There was a problem signing in. Please try again.' + redirect_to root_path and return + end + access_token = tokens_response['access_token'] session[:id_token] = tokens_response['id_token'] user_info_response = auth_service.user_info(access_token) + if user_info_response.empty? || user_info_response['email'].blank? + flash[:alert] = 'There was a problem signing in. Please try again.' + redirect_to root_path and return + end @email = user_info_response['email'] - gov_user = find_or_create_user sign_in_and_redirect gov_user if gov_user end @@ -18,6 +26,24 @@ def openid_connect # @return [User] def find_or_create_user - User.find_by(email: @email) || create_from_email(@email) + User.find_by(email: @email) || User.create_from_email(@email) + end + + def after_sign_in_path_for(resource) + if resource.registration_complete? + if resource.display_whats_new? + resource.display_whats_new = false + resource.save! + static_path('whats-new') + elsif !resource.email_preferences_complete? + static_path('email-preferences') + else + my_modules_path + end + elsif resource.private_beta_registration_complete? + static_path('new-registration') + else + edit_registration_name_path + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 2cbec1b77..03dfc932f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,7 +24,7 @@ def self.dashboard_headers # @return [User] def self.create_from_email(email) user = User.new(email: email, confirmed_at: Time.zone.now) - user.save(validate: false) + user.save!(validate: false) user end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index ff86c9765..79a1c733a 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -22,6 +22,9 @@ def tokens token_response = http.request(token_request) JSON.parse(token_response.body) + rescue StandardError => e + Rails.logger.error "GovOneAuthService.tokens: #{e.message}" + {} end # @param access_token [String] @@ -34,6 +37,9 @@ def user_info(access_token) userinfo_response = http.request(userinfo_request) JSON.parse(userinfo_response.body) + rescue StandardError => e + Rails.logger.error "GovOneAuthService.user_info: #{e.message}" + {} end private @@ -48,16 +54,15 @@ def build_http(uri) # @return [String] def jwt_assertion - rsa_public = OpenSSL::PKey::RSA.new(File.read('public_key.pem')) - rsa_private = OpenSSL::PKey::RSA.new(File.read('private_key.pem')) + rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) payload = { aud: "#{ENV['GOV_ONE_BASE_URI']}/token", iss: ENV['GOV_ONE_CLIENT_ID'], sub: ENV['GOV_ONE_CLIENT_ID'], - exp: Time.now.to_i + 5 * 60, + exp: Time.zone.now.to_i + 5 * 60, jti: SecureRandom.uuid, - iat: Time.now.to_i, + iat: Time.zone.now.to_i, } JWT.encode payload, rsa_private, 'RS256' diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index b9a694e94..e222c47db 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -37,4 +37,9 @@ = content_resource 'home.login' .govuk-button-group + .govuk-button-group + = govuk_button_link_to 'Sign in', new_user_session_path + .white-space-pre-wrap= ' or ' + = govuk_link_to 'create an account', new_user_registration_path + .white-space-pre-wrap= ' or ' = govuk_button_link_to 'Sign in with Gov One Login', login_uri diff --git a/config/application.rb b/config/application.rb index eb6040602..7e88832b9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -67,6 +67,9 @@ class Application < Rails::Application config.contentful_management_access_token = ENV.fetch('CONTENTFUL_MANAGEMENT_TOKEN', credentials.dig(:contentful, :management_access_token)) # TODO: use service account management token config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) + # Gov one + config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) + # @return [Boolean] def live? ENV['WORKSPACE'].eql?('production') diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index 4fdfcc7f6..b4fe3c9ae 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -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--U4dwbZHHF88MfY4i--tryRPvAAHYaJFB+11sqGZQ== \ No newline at end of file +Y+EgIqYzaAxckrOE38ArevcqOV3rUrkTDRDDPzNBJnT87ckbDtlB2/Uhyvv/n29dvTRtAX6eQXAflgZL1EmGuZgyTZoE42/nAgYJ3RxER5ALL+Qa8t9oxiZTRT4ujbe7lmw6R9dmUOV/zMaYnxKT6OczkmWmipyJoUxTtmeoQKwnee7JX8rn5g5Umpu+dWFiA9JXxiAWOlyu+fRWyLObTceE07dHlwEXioTQvzq+4xnhNQ8Tx3SOZhSQXSKM1awr/j5snKdW1T56A9CybOhkzTV6FKa+H1i4YteHUfAXUd2JfDahynNOkQ/bfMP3y2hNhKgmaWum8PDNqlBo+WSNyYfmilpxeuSCIFNtFFvL3uus9/lFlQUqkiTw0wqHWK+FawGXcViakgcix9Vc+nLOlVfWvHgzGZ0KS0BUfKZ0k+hP/xIhr+FgkXR/PSSpC48f192DFqcZX72yQqTiRxtEVqEOEhsrY80DQPMZ0kcMho8s0uLQuz87qj5z2uS1VP+S/eVNw9D6F8W3FcCVw5UrNuJ+p3K/HA04cMLoQvIEtiMH3eUQJBlYdwxuRAdJmBuykr4vZo1eUu4t+vzRgSGHVLuWrlRSZS0FcJzJZkVyTmaf5iVm+jlMf1u9pQCFCQRycAePVW+MRJZ1lS3rbbcBvdtaSZT2ybRjvhHsfROp/H4/E0PBL2q13pBcLDaaHgXvJtjV/lKaFhm8XIDw5P8pmmzC5YXQ4k48n8zZwJcXBW8mAouvZBA9Lqcsjro0JnKFd+FJkvgNdq3pVcnYLXVRwsvy60HqKLbfCUW1XAYq4i/OQ+doUWShSuBeTdeMf34uuZ43fQaySXgloD1SUjbCx1tqEsZKxkQhRgxOkowS1fyYqV7OuvTKIZke23teWhOPR1I+nkLzUGO6l7HONrKZSC42/0jtakQKRknIROi+/vvkFP49Y5cQo3+d+HhPIdQrB+/HQluTgnTBjky9SVjGTFG9W700pXSxcdL0RzDjqd9yQSXURhFkm6A4HOrM20nitacVF+FaLF06MxCNwuwnUy5/NMDLawginnMQCScnNzfCxayXO7mwmYELowAdClonf8YFpejnKcSbMLcijTgW17ihh88ItjmLK1y4DBuUZ93AyD8rx8uWdTrEpQBCWehnCv6YzxoS2BLZqLPM1UUZjyAVQE/t4Mk7QOCuLyND7f8DVxPikxD0UH16wXOA2DSMOcxf/LfqKOlLsTedx2pEzzD+SJK9UeUED2OJh4aLzX8btuguiZQ+7DPvYwciG3lBktTUOiP5rCX4UxhDra9/Muw5DOKBqLIqoCn7AfXFuOPy+HpRGT+M8l/e3RpSMR6AO+5OERGpqNSRc+uuIzEMsw/JosyFXv+uyqQVksjwMYi25h9HGtAyEPNnDfluCifHAVY/zB0065kdN2xxorlrhXXico0/yVu6Z5M+KVpkKj2shqsCjCWY1iWy7EyGxi/TYw/JIspIlK+2xOLbbQfNytfNRU1I3TxXlEyc9IwOneqb3weZninvGMXym5t40NIudabTlTnKjlivissXRT3qdI80uY3L1I4aIIBqovkNXJAYrY7lBvczjqWenlu69xiAkQDjVslLBZPwlLZZB4lzIM1NgJWVPV+EqNM/rUD5uUhrDP8Cjo6JJ6khTgq3PhwbdmfBxwYFUJhUNbUQpa+F+YKj1xR+HaCIMETh48pOwpl4gMEqhaNb/XKsET0tRPYMaOCdw8GNgBTuyK4sYuFYwJHB0xXI8URnN3EkgwfSwbUW4zCxb/B1rJ3t1HK4+ou0eaEMtIPaqcqFEzdZtyF/cYlXyZZQFTXTqYaUoS6Ct3dSA2O5uRpjkDh4zInx2Bkxa+kODL0boaRjgWXb5i6DgvswszHig/ZYhTIZJ8zZLjZsOkcZzhU72O/prFeeqeocp4t9evYSIrsm5436pA1eRFhEcqkRA+zwhKjtQXSFRNBxh0ahBlWSHPY8PnYF+0nGnrNI1tDnKrVgJ2dHEBBv4dcB7FExlRf7VhkZOdYsnJcGuy3hMyHo7bVx8dtgK28tbgZAE6E8AfnrMCmgn8sgopW/Ds0Ne5zgFojoIfMtO4WTWCPUlELu3+qO/8j3Ig+V30IXayBY6KPS0i1OStnw9x8hrNfyXr425Z4hJMyGcd9ux89f7ulhiGh8BaXuEK1Iyktww8zW+2ipUZ28IblQ80x1ywXPSE2pvtiSDDV7EV8wjM+7qnsXgdoK8jTAw3H5aInXZeW+WHNH8H2Fu1RdeMUnXY3HNb5MkY+NTpEutu2ewiGzNkQIXoqGYO8TIdnjGphJE5Ui56K7v5kikuhk00KFN36Ts1oBBBUpCBUT3Om9qbnp4H4VxxnTERyJcE1iHeMjSxABnxhyPmJhOxIVXIr9MJRkWj3A/JgUEXFQASZIVXSJmxSljuhEu3Gck3ZCy9fD3pB3OzUt6AKSB1x6/HazjruYkoXpuLpybClf/uthj4LvPK5HlnkR2sUFjFwU5aSxTvuVVBQe8XWHa7Mp6zx5G2Ri2M0dQ+b3dBuMI1tvVNZw3t0z6FTtNzvyW2wM9ewg3LsDoqi7d8Gab2faOGTJTARabCiKJFBEnJL0X8mOWjnvP0+G1sVsfLvlTHkrbCl8d3udlzRuBSvxaH+NkkqUDFKmPqUpUff9jPrC2sgUDx8B8lhpG1AJ7MKEXncEdnM299d4+RC/sEGLeMlQCwJujQ9IptxDPX9JD04ZAuHRNGakpJqlD1nsQwmC2d6nJpP9DiXhp5T+kK7i7cmWhuIi5Bs9EeeotBN+V2F/atyjzFafQYmLgSv+EMGSdpiadvosugWFdyZrk+7wCIeU1md4LfEFK+FKhZdBaWVcMkvAncnYkPa6Xtalmt8RshRKndRYrgD6OZ3RE6p7HfPElhvqi1cN7vg+tGt2dzNSmCQWMe6MtogRZdGy4g/pl24f+Dc76e3FG2vF/x8yeFsxpK5gPw0nQ0mQvgamXjEAOiSjs8TP62NEzvP9idnHbwPNZ/yGMoToiZoKnWV2GPiL2KaLYjpE69vxotmUZgPuDUd2TtzvgXreeXR4JilgNXq+E9UyeQDZCgjEyXw054taxnxy32CKars/8Z0CFjxXbHztGOUDjeInwxZSzCIzEibMkffvMPGFeP9lts6FUsa3oOmJCG3NFAJ/ayyYSths+FzPAHAKkx6PN458wbJt0BylLR5qqvV8otofPSwVs6b1jf6cwxmp8O5iPWdacwqGJ4hkXkIa7s1V+2/68jkGN6XtZjJWjbvewrggpXChn5zPGo6kudtStY3RpvQ6hQNysbFWfwa1yHkeQXGmOxDNvTF5p7OPOKInc9Vmhe/490Er8iuv896KgUOtrzund1CGsjmFxrz5CN6DVplaqEzjUrPtqD0H1Yg/XSM7xMJGZ7vgeEAIBk9dEM21bAoxD5DOfUuPBOmSKTJdZ824uDHhJ5ScIBiTIjsUipetP5KjCUjrLzeS3t8J0anW9V7UP5wAZ2rEGQX/w8YlewErz0Sw6WnrQpryGbPFgLFPWBUwM5nZY+Gu0evrNWLunKtD6N7IMatb1YZuOyjDrEE3PqAHw6zgQANFqsd7F6voxiK4ku8YjIOFmMMfQyyNxO20DwtyQgLLbnOpqOAIpynEXNPQPn4eR0eotyGeiPrnYnzXc9LlSlMsJWKFCOR5DDrsxbcrAE+S4kyxf6CrbSSGNux6XzoSOFnH3qwXYbgBF4jwCbFvagndCutfTei9Q0oScdLZIkLYyee22so7TqaWFZ++uNz6NDltAJP7JxRtJly1pL8G+jP9g8SYPYP2gVwC/Ff3m0TqUK3ZdGDb7b6lTbJ+YR43KuwCTuFglomqxW9Vad5QFFmI7zQLB1Pvi18HbWoO8oxcXNC+69Hl4IPX7raEax2d9zviA2eeCjjRtjGvhmXOJgdY4WCrs/wYKuJAzBWxVXv8333dSEkbWzI1KSkdJN0zq3NHv4AQ/rPcj3ubjjr1bm8I0wr41aZIVxP9LLmbXCDi/+UJkRhtXRBZbZJgqGb0p9hMh4jzhl5zTy8how4D5lCDZM2bzgYcsKCrCCu+TufG1xZ3SisWiJeW9TsDXG4X8QGU4gZ0iI1TZGxHsMUpVHQgeZsovPMFPUm6ZV+kkXe/Br1UNcWtFNZPq3U/r2zZn9nGhqZ+aqN8nhIMrDsC2tInQOg/0mgPUSzzyQS/bUle4eJXXDsoFdSP15FdMwtD2F5nU3o/5Ye5i7CuNbZSXtQh7HzxYBqcgzEOFDgxu5GHiQdnvKClGzlR4n1ch/qHcKEkVxk8xFVvHv0P7jTZtM83MHBUNAVjofZLJJetVd4SQsb+1y0BcQwlFI4rn/AELQhkBZTaVc5evPM8cW2ia60YpfoAXh7uRSZqKQ1frtGljWQTqGZ8AJsNaIfoJPq+02BYXWC2ilbf7Y24G1fOXHY8lJgDNYk1QdTZYM1wtgq9NoCSMxxoajQ7SSniAcOdkH7uAAE2ZACCjELwZvJiQGoKL86RwfTV9U+/YDPbw+9qLKZK33znqx4NpYUDuS4fC3u1NE8VdT1OCJMjqG8ovNKOnvqlBpHxM7g4NFo0x55RWLCOpNQrZshgtwkiqGUAs2YO3fp6MxpcIgcn9E1wYtnx7+09hfxSG6lRpHtXoD8CfxtwQw2Xqq5iBGP8UTyH/6m9apfpHjuM+VaEOGe+GWjuBYXsCOfklXg2THp7vTbehKNfpscd0QjJLZbzMBpZnUVS9Qg4+Dw4/INggg1jOci/PpruqshJSURlVcXN2l5YxJ3LXmPC8c468tWbaMIW1dFBLWXU1cU/ZlXL0Ke4MK3WjwoM0Eya1zfA2cUNSHFAkMzmh5oY3l0Iwn49mvOPPdcsOmPqCm5PgofACIYMf0La7kcBKCoxk7V0mBSYKwa/TLSHJsX8YRTp0XVCOoGbeis77aXRlbNr9OYxloDhDD7eQOq8R9OWU5n/25oYwqGY7PXYfsxpb3427IBq65PU3CFWcnepJaHlkt+7uOyT/Nx4JapsUYVv30e+SGs6HQlUww/4u0lNgOz3OX2qFpSD0swFjcDcyT7IvlSk/vlpkMIqUo6CQ9eMgWK82hqQtxW4A6moNb+Ioy6GEUF4CRQmOqq2DQOYWiZ/jvz/Xt6y10fXEaZUf1r7v4ar/Kv58GohMcbGMqlQixRqsg/17ouCCkF/QFWN4LXeOO/IvRUprL21EyLLg44npGTek3LSUZ599IhlqtOgTy9AFBesU7ABzEvqcHkP2z8YNJphF+WXxVhY9FbJx5LmQKnWhPNRJ2mxXmGLL3f6NXn3micUDBlRtPKqBQ7Lcass+Oo8dAw0RYoAnIGg4dzYt8pLROSTXrdy7JNMW018oPKkbiYMy7LD+mnyR0o+3G4hfkSaaifXkh7tgnN7NZ8k0S8RIhHhC+bNrWJzXyo4cDhn/iu602Ud2go9Q4rrgT04C8Jhe7+Gcn/haeFVGLbZQq8U9LvbxrhlgVNnrMDi5p6LPZMm7QBDR7IW8xisuMu7DEksRRyZCF5phkb4I0sPfPpOYlkojgYu4CjHSw0BvJVkAmKOXI/UETfFtNNJV9pRzDf/DQLdXf2WI3MITsgnFqnzrZMwVYUbKl9PIOUbnpMBzH7qxZKaEsYS0wEB+c6lCrWmkEXXk7W7cj9CCxVHaGColbx5eNkpiEPdQj5ngmRRwDmMgSpJkGWYff1dT7bIG/b7Ibul4dbXCo/TRnWx9aZhEhqyV4omARp1JThMjuyuFwUMG7yZ8ULcuam5Pe/wOwoUxTQ6If7xtiAcjduOEXtuo6q2CBs6/9hk7wUB998OVReJHpdfOxg7k7lRt/QHPFjhchRFjOpzocFYjGcC1ZIymaXAsusumyHYbO6JLcUqyK99gpNjG5KTJsiNL05H/9d9LOz6f9+Kps4wGCFqS7RC4LJvCP1wOXGLvpr0sdl66Fc8MxSqYvagcdFFvLVUoE/MTcFgicAtIRzydjUfE9MHma5KJagAHAXl1rlBM9s51shI2kQaThACI52DB403RmTv9KQwql01eV9CJKXKGkqWPIKvFPJidGXhD8Nfv20Husmc0B6cshgiJ0OcDuTNvGCctBmkRZ/d+D5Bc9tDG4VAK9wBA2mpda8grurKr+kdn48mV2vX9LrTo8gpNoYGY/cn8z8vNNs8ymxj4q/MhV3F8Uh8Y5/sL60UOAY4eGw6FaysvA8c4A59mik6NymJ1AXMu8h377CY9wUPbXAWwperOo2asuRdBfVNCFKQro6axzZA8VjijsHZFmfa201vLSXbjLIyvPQgDUL4C5buWIbvkUSBNFVUKlscBA2qIy5dgCX/NzoR/ncLJ4du5dHFKCLn0H8RUsPvOD1E1BgCtm7pDHaduDtySLSxltgfN29UrE5M1X0qW8xRvSojGlwKR/ZU7RX0uCAlQHQASAOZz8SGzvmjYleTiimgX30Vyyyl1amjHXVgjTrqvsDuChw4he8J/yDNozqK1SJElBf4xnfxl+zoitxrzLbW17CxOnon+M4WVvemrbw0Hygv9ft4M0C36h62iLIlXzu+h/uaqdtbz0biqhMNlc+yOldN1HRRG54lt33ZGlMwRXptT9ed7vv3NrxZGbU2OVXLDf9CwzUxVaQwoYaMHM7WrOMD1I2atNRtEw+XHLXvsgDZ7xY807pEbKHHqP9xjFy32TrEV2cMP7wVQrKK7bW7loStYghYvSvKGhx+76Q3Mv1QEs77q+rv/BVBrLP7YRH4y35/ecakyiIydwO7YIdBB3Jyx7zEqmyP+AecZjHSmlDz76z7/sG9AqCf3oA8nS7H/O8ey3Pgnq9zT5qpvwAzQbCRgswhTDnTfykss9mVzm1eOEK7ZpIUGqnzf3HZS6D4Sj4TvGBolmhIYoMrKGh2nimiIf/1HwolZV+2Wwh1e90fCnuJKnNkurPw+29UXNtmicqfCir+/2RXW++7Giwaa3yFHwoA/ZvHA/087ORREm8WDosBoR+Q+0f0EvsblNfGKJ4d45+YWjlRt3RHCVHg1x+4CSwrEiSKf6n0wkVb9OQMnzymIKzaIyCB1a0RnBF4tCB13SY1zcIGt6jxQU65ZPhZYHxXPCO6oKGDAtgiu/sTGiYLprElfCUVIeVS8wfu0CPjB0B95GcNEMsfh/j+Iu6e1lY8qeT4NJQ5tAxX6FBBG+fXUhB1megQE+luQGHIgH6lTz6Xn+NSC7yaN8ffFPiK4Tl8UOE+pKOnx7hU3/cT6B2N5iAUGzhf8N3n0zKPlX1WOgEN8ZW4PBgSAuV3hKxjQcunoj5FZLHK+6GNIL2vSz+Jz6w09Vg54gz/tPnowWHt7EB6NIJDOYCqlWDz7b4Y/+eCRL8zvvwZccgt07bmFsISL6nAS/u97rQMYieE7N4E139GUsy+oYaTzQSWYFV6byao9nu1thDo91kF2/w+6F9wZ2r61+K+1gww83+luuT7vbWb9MgFTydxbvZg792tQFif0NPPTEYbv4aSUeYNFvJqheX++8JBQ1UqmxTw5LWpgm3icCDl8lIyGqjKeCgfl0BJ3SYrDKudvDfCK+CKLuMYPixkRz907huxcDeuDi7RP/HcY0lxA7HafE4LVDSOiIHgUcVwYAhMl8dFvKdz0InlXOtgaRGdX1+aoiRrHmTIGlS860iTPuob0VVTL8SIWvrQW963X7bU+ajwQkPnO7v/XHcsJg0L8hPt1ZGVpdcs8V85p6RuipLqsKeA7nu3gCau7S3SW8pZL8FnzUJLNbfij9ud1hMPQmqatYgO4nk27605ddn1zxB8XzvQTbWLY4vA49P3BoQQAxCx80QK2gzhWDpWAp9IjMlkYUeUjIA/xR0RKzFi6suJ7D6v0dTkltO7Um4BedfkdeXLO/BhPyrw0pBcLBbJVtXvEq0SfxL6XrSTqGQrz5NhnS3TSdBdZs15mQ+B15e3tXPuTNAzzHrA+GmDlf0vrKrPCxwu--bjV860iSph86biSM--eO2U1UjfoPVUyTzhWcIu9A== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index dbc4d4155..a95dc679b 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--xd1sesNNvCLgfxxi--Uw8M7itth9vCF+0j3D+j0w== \ No newline at end of file +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--o03/wyKMqS37wmEr--PGWkhEX04SURN12/rZeEDQ== \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index 5ee9a3078..9f2b7b14a 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -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--524FMqbKrVmJ2sZ3--G4/cI/16dU0QC5Cp94Fn7g== \ No newline at end of file +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--F7LPrF7SEIYJasNO--J/H0MGGLiOWbI22U2DGy8g== \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index cfbf68e79..c50ca7eb3 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -315,7 +315,6 @@ def i18n_message(default = nil) scheme: 'https', host: 'oidc.integration.account.gov.uk', identifier: ENV['GOV_ONE_CLIENT_ID'], - secret: ENV['GOV_ONE_CLIENT_SECRET'], redirect_uri: 'users/auth/openid_connect/callback', }, authorize_params: { diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 85875a3a9..2d768fef8 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -1,10 +1,10 @@ require 'rails_helper' RSpec.describe Users::OmniauthCallbacksController, type: :controller do - before do - @request.env['devise.mapping'] = Devise.mappings[:user] + request.env['devise.mapping'] = Devise.mappings[:user] end + describe '#openid_connect' do let(:auth_service) { instance_double(GovOneAuthService) } let(:access_token) { 'mock_access_token' } @@ -13,36 +13,30 @@ before do allow(GovOneAuthService).to receive(:new).and_return(auth_service) - allow(auth_service).to receive(:tokens).and_return('access_token' => access_token, 'id_token' => id_token) - allow(auth_service).to receive(:user_info).with(access_token).and_return('email' => email) + allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) + allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) + allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') end - it 'sets session id_token and finds or creates the user' do - get :openid_connect, params: { 'code' => 'mock_code' } - - expect(session[:id_token]).to eq(id_token) - expect(assigns(:email)).to eq(email) - expect(response).to redirect_to(root_path) + context 'when a User does not exist' do + it 'sets session id_token and creates the user' do + get :openid_connect, params: { 'code' => 'mock_code' } + expect(session[:id_token]).to eq(id_token) + expect(User.find_by(email: email)).to be_truthy + expect(response).to redirect_to(edit_registration_name_path) + end end - it 'signs in and redirects the user if found or created' do - user = create(:user, email: email) # Assuming you have a User model and FactoryBot - - allow(controller).to receive(:find_or_create_user).and_return(user) - - get :openid_connect, params: { 'code' => 'mock_code' } - - expect(response).to redirect_to(root_path) # Adjust the path based on your actual redirect path - expect(subject.current_user).to eq(user) - end - - it 'does not sign in if user is not found or created' do - allow(controller).to receive(:find_or_create_user).and_return(nil) - - get :openid_connect, params: { 'code' => 'mock_code' } + context 'when a User exists' do + before do + create :user, :registered, email: email + end - expect(response).to redirect_to(root_path) # Adjust the path based on your actual redirect path - expect(subject.current_user).to be_nil + it 'sets session id_token and signs in the user' do + get :openid_connect, params: { 'code' => 'mock_code' } + expect(session[:id_token]).to eq(id_token) + expect(response).to redirect_to(my_modules_path) + end end end end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index efad75a26..da99a8ef8 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -1,47 +1,46 @@ require 'rails_helper' -RSpec.describe Users::OmniauthCallbacksController, type: :controller do +RSpec.describe Users::SessionsController, type: :controller do before do request.env['devise.mapping'] = Devise.mappings[:user] - get :openid_connect, params: { 'code' => 'mock_code' } + post :create, params: { user: { email: user.email, password: user.password } } end - let(:auth_service) { instance_double(GovOneAuthService) } - let(:access_token) { 'mock_access_token' } - let(:id_token) { 'mock_id_token' } - let(:email) { 'test@example.com' } + let(:user) { create :user, :registered } - before do - allow(GovOneAuthService).to receive(:new).and_return(auth_service) - allow(auth_service).to receive(:tokens).and_return('access_token' => access_token, 'id_token' => id_token) - allow(auth_service).to receive(:user_info).with(access_token).and_return('email' => email) + context 'when registration is complete' do + it 'continues to their training overview' do + expect(response).to redirect_to '/my-modules' + end end - context 'when user is found or created' do - let(:user) { create(:user, email: email) } + context 'when new feature alert is enabled' do + let(:user) do + create :user, :registered, display_whats_new: true + end - before do - allow(controller).to receive(:find_or_create_user).and_return(user) + it 'redirects to /whats-new' do + expect(response).to redirect_to '/whats-new' end + end - it 'sets session id_token' do - expect(session[:id_token]).to eq(id_token) + context 'when email preferences are not defined' do + let(:user) do + create :user, :registered, training_emails: nil end - it 'signs in the user and redirects to root path' do - expect(response).to redirect_to(root_path) - expect(subject.current_user).to eq(user) + it 'redirects to email preferences form' do + expect(response).to redirect_to '/email-preferences' end end - context 'when user is not found or created' do - before do - allow(controller).to receive(:find_or_create_user).and_return(nil) + context 'when registered during private beta' do + let(:user) do + create :user, :registered, registration_complete: false, private_beta_registration_complete: true end - it 'does not sign in and redirects to root path' do - expect(response).to redirect_to(sign_in_path) - expect(subject.current_user).to be_nil + it 'redirects to new registration' do + expect(response).to redirect_to '/new-registration' end end end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 79a6af1d8..0b978ee59 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -1,52 +1,55 @@ require 'rails_helper' - -RSpec.describe GovOneAuthService, type: :service do - let(:code) { 'mock_code' } - let(:access_token) { 'mock_access_token' } - let(:id_token) { 'mock_id_token' } - let(:user_info) { { 'email' => 'test@example.com' } } - - before do - allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('http://example.com/callback') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('urn:ietf:params:oauth:client-assertion-type:jwt-bearer') - allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('http://gov-one-example.com') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('your_client_id') - - allow(File).to receive(:read).and_call_original - allow(File).to receive(:read).with('public_key.pem').and_return('mock_public_key_content') - allow(File).to receive(:read).with('private_key.pem').and_return('mock_private_key_content') - - allow(SecureRandom).to receive(:uuid).and_return('mock_uuid') - - @auth_service = described_class.new(code) - end - - describe '#tokens' do - it 'returns a hash with access_token and id_token' do - allow(@auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') - - token_response = { 'access_token' => access_token, 'id_token' => id_token } - - http_double = instance_double(Net::HTTP, request: instance_double(Net::HTTPResponse, body: token_response.to_json)) - allow(@auth_service).to receive(:build_http).and_return(http_double) - - result = @auth_service.tokens - - expect(result).to be_a(Hash) - expect(result['access_token']).to eq(access_token) - expect(result['id_token']).to eq(id_token) +# require 'webmock/rspec' + +RSpec.describe GovOneAuthService do + skip 'wip' do + let(:code) { 'mock_code' } + let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('mock_redirect_uri') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('mock_assertion_type') + allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') end - end - - describe '#user_info' do - it 'returns user information hash' do - http_double = instance_double(Net::HTTP, request: instance_double(Net::HTTPResponse, body: user_info.to_json)) - allow(@auth_service).to receive(:build_http).and_return(http_double) - result = @auth_service.user_info(access_token) + describe '#tokens' do + it 'returns a hash of tokens' do + stub_request(:post, 'https://example.com/token') + .with( + body: { + grant_type: 'authorization_code', + code: code, + redirect_uri: 'mock_redirect_uri', + client_assertion_type: 'mock_assertion_type', + client_assertion: anything, + }, + headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }, + ) + .to_return(status: 200, body: token_payload.to_json) + + auth_service = described_class.new(code) + tokens = auth_service.tokens + expect(tokens).to eq(token_payload) + end + end - expect(result).to be_a(Hash) - expect(result['email']).to eq('test@example.com') + describe '#user_info' do + let(:access_token) { 'mock_access_token' } + let(:user_info_payload) { { 'email' => 'test@test.com' } } + + it 'returns a hash of user info' do + stub_request(:get, 'https://example.com/userinfo') + .with( + headers: { 'Authorization' => "Bearer #{access_token}" }, + ) + .to_return(status: 200, body: user_info_payload.to_json) + + auth_service = described_class.new(code) + user_info = auth_service.user_info(access_token) + expect(user_info).to eq(user_info_payload) + end end end end From b2b8263e0aface01c39b0dd0a04987fbbf967252 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 24 Oct 2023 13:28:20 +0100 Subject: [PATCH 003/122] remove existing sign in options from home page --- app/views/home/index.html.slim | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 8850cb22f..73b8d82ca 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -38,8 +38,4 @@ .govuk-button-group .govuk-button-group - = govuk_button_link_to 'Sign in', new_user_session_path - .white-space-pre-wrap= ' or ' - = govuk_link_to 'create an account', new_user_registration_path - .white-space-pre-wrap= ' or ' = govuk_button_link_to 'Sign in with Gov One Login', login_uri From 1c0fb290ae6e35674acdc7b8f9cf53ffe2984b3c Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 27 Oct 2023 10:47:10 +0100 Subject: [PATCH 004/122] wip --- app/services/gov_one_auth_service.rb | 2 +- spec/services/gov_one_auth_service_spec.rb | 65 ++++++++++++---------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 79a1c733a..8e6362e74 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -11,7 +11,7 @@ def tokens code: @code, redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], client_assertion_type: ENV['GOV_ONE_CLIENT_ASSERTION_TYPE'], - client_assertion: jwt_assertion, + client_assertion: ENV['GOV_ONE_CLIENT_ASSERTION'] || jwt_assertion, } token_uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 0b978ee59..18d445fdb 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' -# require 'webmock/rspec' RSpec.describe GovOneAuthService do - skip 'wip' do let(:code) { 'mock_code' } let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } @@ -12,44 +10,53 @@ allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('mock_assertion_type') allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION').and_return('mock_client_assertion') end describe '#tokens' do it 'returns a hash of tokens' do - stub_request(:post, 'https://example.com/token') - .with( - body: { - grant_type: 'authorization_code', - code: code, - redirect_uri: 'mock_redirect_uri', - client_assertion_type: 'mock_assertion_type', - client_assertion: anything, - }, - headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }, - ) - .to_return(status: 200, body: token_payload.to_json) - - auth_service = described_class.new(code) - tokens = auth_service.tokens - expect(tokens).to eq(token_payload) + mock_response = double('response') + allow(mock_response).to receive(:body).and_return(token_payload.to_json) + allow(mock_response).to receive(:code).and_return(200) + + mock_net_http_post = double("Net::HTTP::Post") + allow(Net::HTTP).to receive(:new).and_return(mock_net_http_post) + allow(mock_net_http_post).to receive(:use_ssl=).with(true) + + allow(Net::HTTP::Post).to receive(:new).and_return(mock_net_http_post) + + expect(mock_net_http_post).to receive(:set_form_data).with({ + :client_assertion => "mock_client_assertion", + :client_assertion_type => "mock_assertion_type", + :code => "mock_code", + :grant_type => "authorization_code", + :redirect_uri => "mock_redirect_uri" + }) + allow(mock_net_http_post).to receive(:request).and_return(mock_response) + expect(described_class.new(code).tokens).to eq(token_payload) + end end + describe '#user_info' do + it 'returns a hash of user info' do let(:access_token) { 'mock_access_token' } let(:user_info_payload) { { 'email' => 'test@test.com' } } + mock_response = double('response') + allow(mock_response).to receive(:body).and_return(user_info_payload.to_json) + allow(mock_response).to receive(:code).and_return(200) + + mock_net_http_get = double("Net::HTTP::Get") + allow(Net::HTTP).to receive(:new).and_return(mock_net_http_get) + allow(mock_net_http_get).to receive(:use_ssl=).with(true) + + allow(Net::HTTP::Get).to receive(:new).and_return(mock_net_http_get) + + allow(mock_net_http_get).to receive(:request).and_return(mock_response) + expect(described_class.new(code).user_info(access_token)).to eq(user_info_payload) - it 'returns a hash of user info' do - stub_request(:get, 'https://example.com/userinfo') - .with( - headers: { 'Authorization' => "Bearer #{access_token}" }, - ) - .to_return(status: 200, body: user_info_payload.to_json) - - auth_service = described_class.new(code) - user_info = auth_service.user_info(access_token) - expect(user_info).to eq(user_info_payload) - end end end + end From a51cb32c7e0104f5f964fd5fbef04ff3b8511809 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 27 Oct 2023 15:52:40 +0100 Subject: [PATCH 005/122] update auth service specs --- app/services/gov_one_auth_service.rb | 2 +- spec/services/gov_one_auth_service_spec.rb | 82 +++++++++++----------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 8e6362e74..4e991aef7 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -48,7 +48,7 @@ def user_info(access_token) # @return [Net::HTTP] def build_http(uri) http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true + http.use_ssl = true unless Rails.env.test? http end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 18d445fdb..deb011eb1 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -1,62 +1,60 @@ require 'rails_helper' RSpec.describe GovOneAuthService do - let(:code) { 'mock_code' } - let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } - - before do - allow(ENV).to receive(:[]).and_call_original - allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('mock_redirect_uri') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('mock_assertion_type') - allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION').and_return('mock_client_assertion') - end + let(:code) { 'mock_code' } + let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } + let(:mock_response) { instance_double('response') } + let(:auth_service) { described_class.new(code) } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('mock_redirect_uri') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('mock_assertion_type') + allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') + allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION').and_return('mock_client_assertion') + end - describe '#tokens' do + describe '#tokens' do + let(:mock_net_http_post) { instance_double('Net::HTTP::Post', request: nil) } + + context 'when the request is successful' do it 'returns a hash of tokens' do - mock_response = double('response') allow(mock_response).to receive(:body).and_return(token_payload.to_json) allow(mock_response).to receive(:code).and_return(200) - - mock_net_http_post = double("Net::HTTP::Post") + allow(mock_net_http_post).to receive(:request).and_return(mock_response) allow(Net::HTTP).to receive(:new).and_return(mock_net_http_post) - allow(mock_net_http_post).to receive(:use_ssl=).with(true) - - allow(Net::HTTP::Post).to receive(:new).and_return(mock_net_http_post) + + result = auth_service.tokens expect(mock_net_http_post).to receive(:set_form_data).with({ - :client_assertion => "mock_client_assertion", - :client_assertion_type => "mock_assertion_type", - :code => "mock_code", - :grant_type => "authorization_code", - :redirect_uri => "mock_redirect_uri" + client_assertion: 'mock_client_assertion', + client_assertion_type: 'mock_assertion_type', + code: 'mock_code', + grant_type: 'authorization_code', + redirect_uri: 'mock_redirect_uri', }) - allow(mock_net_http_post).to receive(:request).and_return(mock_response) - expect(described_class.new(code).tokens).to eq(token_payload) - + expect(result).to eq(token_payload) end end - - - describe '#user_info' do - it 'returns a hash of user info' do - let(:access_token) { 'mock_access_token' } - let(:user_info_payload) { { 'email' => 'test@test.com' } } - mock_response = double('response') - allow(mock_response).to receive(:body).and_return(user_info_payload.to_json) - allow(mock_response).to receive(:code).and_return(200) + end - mock_net_http_get = double("Net::HTTP::Get") - allow(Net::HTTP).to receive(:new).and_return(mock_net_http_get) - allow(mock_net_http_get).to receive(:use_ssl=).with(true) + describe '#user_info' do + let(:access_token) { 'mock_access_token' } + let(:user_info_payload) { { 'email' => 'test@test.com' } } + let(:mock_net_http_get) { instance_double('Net::HTTP::Get', request: nil) } - allow(Net::HTTP::Get).to receive(:new).and_return(mock_net_http_get) + context 'when the request is successful' do + it 'returns a hash of user info' do + allow(mock_response).to receive(:body).and_return(user_info_payload.to_json) + allow(mock_response).to receive(:code).and_return(200) + allow(mock_net_http_get).to receive(:request).and_return(mock_response) + allow(Net::HTTP).to receive(:new).and_return(mock_net_http_get) - allow(mock_net_http_get).to receive(:request).and_return(mock_response) - expect(described_class.new(code).user_info(access_token)).to eq(user_info_payload) + result = auth_service.user_info(access_token) + expect(result).to eq(user_info_payload) + end end end - end From efd8bcf1de571459aea7660040c9d319336fa528 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 30 Oct 2023 11:53:20 +0000 Subject: [PATCH 006/122] remove user facing gov one login changes --- app/controllers/gov_one_controller.rb | 3 +++ app/helpers/application_helper.rb | 8 ++++---- app/views/gov_one/info.html.slim | 17 +++++++++++++++++ app/views/home/index.html.slim | 5 +++-- config/locales/en.yml | 5 +++++ config/routes.rb | 1 + 6 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 app/controllers/gov_one_controller.rb create mode 100644 app/views/gov_one/info.html.slim diff --git a/app/controllers/gov_one_controller.rb b/app/controllers/gov_one_controller.rb new file mode 100644 index 000000000..8c4f49d46 --- /dev/null +++ b/app/controllers/gov_one_controller.rb @@ -0,0 +1,3 @@ +class GovOneController < ApplicationController + def info; end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8f29d0dbb..e25200506 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,14 +11,14 @@ def navigation header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) if user_signed_in? header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) - header.with_action_link(text: 'Sign out', href: logout_uri, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) + header.with_action_link(text: 'Sign out', href: destroy_user_session_path, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? header.with_navigation_item(text: 'My account', href: user_path, classes: ['dfe-header__navigation-item dfe-header-f-mob']) - header.with_navigation_item(text: 'Sign out', href: logout_uri, options: { data: { turbo_method: :get } }, classes: ['dfe-header__navigation-item dfe-header-f-mob'], html_attributes: { id: 'sign-out-f-mob' }) + header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }, classes: ['dfe-header__navigation-item dfe-header-f-mob'], html_attributes: { id: 'sign-out-f-mob' }) else - header.with_action_link(text: 'Sign in', href: login_uri, options: { inverse: true }) - header.with_navigation_item(text: 'Sign in', href: login_uri, classes: ['dfe-header__navigation-item dfe-header-f-mob']) + header.with_action_link(text: 'Sign in', href: new_user_session_path, options: { inverse: true }) + header.with_navigation_item(text: 'Sign in', href: new_user_session_path, classes: ['dfe-header__navigation-item dfe-header-f-mob']) end end end diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim new file mode 100644 index 000000000..611b35e4f --- /dev/null +++ b/app/views/gov_one/info.html.slim @@ -0,0 +1,17 @@ += render 'learning/cms_debug' + +- content_for :page_title do + = html_title t('gov-one.info.title') + +.h1.govuk-heading-xl + = t('gov-one.info.title') + +.govuk-grid-row + .govuk-grid-column-one-half + = m('gov-one-info.body') + + - if current_user + = govuk_button_link_to 'Sign out of Gov One Login', logout_uri + - else + = govuk_button_link_to 'Sign in with Gov One Login', login_uri + \ No newline at end of file diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 73b8d82ca..b389a981e 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -37,5 +37,6 @@ = m('home.login') .govuk-button-group - .govuk-button-group - = govuk_button_link_to 'Sign in with Gov One Login', login_uri + = govuk_button_link_to 'Sign in', new_user_session_path + .white-space-pre-wrap= ' or ' + = govuk_link_to 'create an account', new_user_registration_path \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 033df49ec..d8c8028b0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -562,6 +562,11 @@ en: %{criteria} + # /gov-one/info + gov-one: + info: + title: Gov One Login + # /settings/cookie-policy cookie_policy: title: Cookie policy diff --git a/config/routes.rb b/config/routes.rb index d1b01e2e5..9de3c4b5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview + get 'gov-one/info', to: 'gov_one#info' get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all From 2c66f3673e4c702a2739b02b32ee0d1aee943a8e Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 30 Oct 2023 15:25:06 +0000 Subject: [PATCH 007/122] update auth service specs --- spec/services/gov_one_auth_service_spec.rb | 40 ++++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index deb011eb1..175844e26 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -2,9 +2,9 @@ RSpec.describe GovOneAuthService do let(:code) { 'mock_code' } - let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } let(:mock_response) { instance_double('response') } let(:auth_service) { described_class.new(code) } + let(:mock_http) { instance_double('Sentry::Net::HTTP') } before do allow(ENV).to receive(:[]).and_call_original @@ -13,48 +13,52 @@ allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION').and_return('mock_client_assertion') + allow(mock_http).to receive(:request).and_return(mock_response) + allow(Net::HTTP).to receive(:new).and_return(mock_http) end describe '#tokens' do - let(:mock_net_http_post) { instance_double('Net::HTTP::Post', request: nil) } + let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } context 'when the request is successful' do it 'returns a hash of tokens' do allow(mock_response).to receive(:body).and_return(token_payload.to_json) - allow(mock_response).to receive(:code).and_return(200) - allow(mock_net_http_post).to receive(:request).and_return(mock_response) - allow(Net::HTTP).to receive(:new).and_return(mock_net_http_post) result = auth_service.tokens - - expect(mock_net_http_post).to receive(:set_form_data).with({ - client_assertion: 'mock_client_assertion', - client_assertion_type: 'mock_assertion_type', - code: 'mock_code', - grant_type: 'authorization_code', - redirect_uri: 'mock_redirect_uri', - }) expect(result).to eq(token_payload) end end + + context 'when the request is unsuccessful' do + it 'returns an empty hash' do + allow(mock_response).to receive(:body).and_return({}.to_json) + + result = auth_service.tokens + expect(result).to eq({}) + end + end end describe '#user_info' do let(:access_token) { 'mock_access_token' } let(:user_info_payload) { { 'email' => 'test@test.com' } } - let(:mock_net_http_get) { instance_double('Net::HTTP::Get', request: nil) } context 'when the request is successful' do it 'returns a hash of user info' do allow(mock_response).to receive(:body).and_return(user_info_payload.to_json) - allow(mock_response).to receive(:code).and_return(200) - allow(mock_net_http_get).to receive(:request).and_return(mock_response) - allow(Net::HTTP).to receive(:new).and_return(mock_net_http_get) result = auth_service.user_info(access_token) - expect(result).to eq(user_info_payload) end end + + context 'when the request is unsuccessful' do + it 'returns an empty hash' do + allow(mock_response).to receive(:body).and_return({}.to_json) + + result = auth_service.user_info(access_token) + expect(result).to eq({}) + end + end end end From e2f22d579568481617ad404511095ba0d615e1a4 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 30 Oct 2023 17:00:36 +0000 Subject: [PATCH 008/122] remove mock client assertion in gov one auth service --- app/services/gov_one_auth_service.rb | 2 +- spec/rails_helper.rb | 2 -- spec/services/gov_one_auth_service_spec.rb | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 4e991aef7..14ba31376 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -11,7 +11,7 @@ def tokens code: @code, redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], client_assertion_type: ENV['GOV_ONE_CLIENT_ASSERTION_TYPE'], - client_assertion: ENV['GOV_ONE_CLIENT_ASSERTION'] || jwt_assertion, + client_assertion: jwt_assertion, } token_uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index da2af60c4..26f221ef7 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -69,6 +69,4 @@ # enable ShowMeTheCookies config.include ShowMeTheCookies, type: :system - - config.include Devise::Test::ControllerHelpers, type: :controller end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 175844e26..db2abbff8 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -12,7 +12,6 @@ allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('mock_assertion_type') allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION').and_return('mock_client_assertion') allow(mock_http).to receive(:request).and_return(mock_response) allow(Net::HTTP).to receive(:new).and_return(mock_http) end From 2e6654590af0fb9defd17e94015231643dd259ae Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 31 Oct 2023 09:16:07 +0000 Subject: [PATCH 009/122] revert gemfile.lock changes --- Gemfile.lock | 265 --------------------------------------------------- 1 file changed, 265 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 04f8fd737..d3a94e790 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,13 +68,11 @@ GEM tzinfo (~> 2.0) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) - aes_key_wrap (1.1.0) ahoy_matey (5.0.1) activesupport (>= 6.1) device_detector (>= 1) safely_block (>= 0.4) ast (2.4.2) - attr_required (1.0.1) backports (3.24.1) base64 (0.1.1) bcrypt (3.1.19) @@ -82,7 +80,6 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bindata (2.4.15) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -122,8 +119,6 @@ GEM contentful_model (~> 1.0) rails (>= 4.2) redcarpet (~> 3.2) - crack (0.4.5) - rexml crass (1.0.6) cssbundling-rails (1.3.3) railties (>= 6.0.0) @@ -185,8 +180,6 @@ GEM base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-follow_redirects (0.3.0) - faraday (>= 1, < 3) faraday-net_http (3.0.2) ffi (1.16.3) ffi-compiler (1.0.1) @@ -249,7 +242,6 @@ GEM grover (1.1.5) combine_pdf (~> 1.0) nokogiri (~> 1.0) - hashdiff (1.0.1) hashie (5.0.0) html-attributes-utils (1.0.2) activesupport (>= 6.1.4.4) @@ -280,12 +272,6 @@ GEM jsbundling-rails (1.2.1) railties (>= 6.0.0) json (2.6.3) - json-jwt (1.16.3) - activesupport (>= 4.2) - aes_key_wrap - bindata - faraday (~> 2.0) - faraday-follow_redirects jwt (2.7.1) language_server-protocol (3.17.0.3) launchy (2.5.2) @@ -329,223 +315,6 @@ GEM racc (~> 1.4) notifications-ruby-client (5.4.0) jwt (>= 1.5, < 3) - omniauth (2.1.1) - hashie (>= 3.4.6) - rack (>= 2.2.3) - rack-protection - omniauth-rails_csrf_protection (1.0.1) - actionpack (>= 4.2) - omniauth (~> 2.0) - omniauth_openid_connect (0.7.1) - omniauth (>= 1.9, < 3) - openid_connect (~> 2.2) - openid_connect (2.2.0) - activemodel - attr_required (>= 1.0.0) - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.16) - net-smtp - rack-oauth2 (~> 2.2) - swd (~> 2.0) - tzinfo - validate_email - validate_url - webfinger (~> 2.0) - opentelemetry-api (1.2.3) - opentelemetry-common (0.20.0) - opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.26.1) - google-protobuf (~> 3.14) - googleapis-common-protos-types (~> 1.3) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-sdk (~> 1.2) - opentelemetry-semantic_conventions - opentelemetry-instrumentation-action_pack (0.7.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-action_view (0.6.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.1) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_job (0.6.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_model_serializers (0.20.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_record (0.6.2) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - ruby2_keywords - opentelemetry-instrumentation-active_support (0.4.2) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-all (0.50.1) - opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) - opentelemetry-instrumentation-aws_sdk (~> 0.5.0) - opentelemetry-instrumentation-bunny (~> 0.21.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.21.1) - opentelemetry-instrumentation-dalli (~> 0.24.1) - opentelemetry-instrumentation-delayed_job (~> 0.21.0) - opentelemetry-instrumentation-ethon (~> 0.21.1) - opentelemetry-instrumentation-excon (~> 0.21.1) - opentelemetry-instrumentation-faraday (~> 0.23.1) - opentelemetry-instrumentation-grape (~> 0.1.3) - opentelemetry-instrumentation-graphql (~> 0.26.2) - opentelemetry-instrumentation-gruf (~> 0.1.0) - opentelemetry-instrumentation-http (~> 0.23.1) - opentelemetry-instrumentation-http_client (~> 0.22.1) - opentelemetry-instrumentation-koala (~> 0.20.1) - opentelemetry-instrumentation-lmdb (~> 0.22.1) - opentelemetry-instrumentation-mongo (~> 0.22.1) - opentelemetry-instrumentation-mysql2 (~> 0.24.2) - opentelemetry-instrumentation-net_http (~> 0.22.1) - opentelemetry-instrumentation-pg (~> 0.25.2) - opentelemetry-instrumentation-que (~> 0.7.0) - opentelemetry-instrumentation-racecar (~> 0.3.0) - opentelemetry-instrumentation-rack (~> 0.23.1) - opentelemetry-instrumentation-rails (~> 0.28.0) - opentelemetry-instrumentation-rake (~> 0.2.1) - opentelemetry-instrumentation-rdkafka (~> 0.4.0) - opentelemetry-instrumentation-redis (~> 0.25.1) - opentelemetry-instrumentation-resque (~> 0.5.0) - opentelemetry-instrumentation-restclient (~> 0.22.1) - opentelemetry-instrumentation-ruby_kafka (~> 0.21.0) - opentelemetry-instrumentation-sidekiq (~> 0.25.0) - opentelemetry-instrumentation-sinatra (~> 0.23.1) - opentelemetry-instrumentation-trilogy (~> 0.56.1) - opentelemetry-instrumentation-aws_sdk (0.5.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-base (0.22.2) - opentelemetry-api (~> 1.0) - opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-bunny (0.21.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-concurrent_ruby (0.21.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-dalli (0.24.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-delayed_job (0.21.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-ethon (0.21.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-excon (0.21.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-faraday (0.23.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-grape (0.1.4) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-graphql (0.26.7) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-gruf (0.1.0) - opentelemetry-api (>= 1.0.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http (0.23.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http_client (0.22.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-koala (0.20.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-lmdb (0.22.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-mongo (0.22.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-mysql2 (0.24.3) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-net_http (0.22.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-pg (0.25.3) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-que (0.7.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-racecar (0.3.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (0.23.4) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rails (0.28.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-action_pack (~> 0.7.0) - opentelemetry-instrumentation-action_view (~> 0.6.0) - opentelemetry-instrumentation-active_job (~> 0.6.0) - opentelemetry-instrumentation-active_record (~> 0.6.1) - opentelemetry-instrumentation-active_support (~> 0.4.1) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rake (0.2.1) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rdkafka (0.4.0) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-redis (0.25.3) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-resque (0.5.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-restclient (0.22.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-ruby_kafka (0.21.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-sidekiq (0.25.0) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-sinatra (0.23.2) - opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-trilogy (0.56.3) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-semantic_conventions (>= 1.8.0) - opentelemetry-registry (0.3.0) - opentelemetry-api (~> 1.1) - opentelemetry-sdk (1.3.0) - opentelemetry-api (~> 1.1) - opentelemetry-common (~> 0.20) - opentelemetry-registry (~> 0.2) - opentelemetry-semantic_conventions - opentelemetry-semantic_conventions (1.10.0) - opentelemetry-api (~> 1.0) orm_adapter (0.5.0) os (1.1.4) pagy (6.1.0) @@ -579,17 +348,6 @@ GEM raabro (1.4.0) racc (1.7.1) rack (2.2.8) - rack-oauth2 (2.2.0) - activesupport - attr_required - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.11.0) - rack (>= 2.1.0) - rack-protection (3.1.0) - rack (~> 2.2, >= 2.2.4) - rack-proxy (0.7.7) - rack rack-test (2.1.0) rack (>= 1.3) rails (7.0.8) @@ -749,11 +507,6 @@ GEM stimulus-rails (1.2.2) railties (>= 6.0.0) stringio (3.0.8) - swd (2.0.2) - activesupport (>= 3) - attr_required (>= 0.0.5) - faraday (~> 2.0) - faraday-follow_redirects temple (0.10.3) thor (1.2.2) tilt (2.3.0) @@ -770,12 +523,6 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.5.0) - validate_email (0.1.6) - activemodel (>= 3.0) - mail (>= 2.2.5) - validate_url (1.0.15) - activemodel (>= 3.0.0) - public_suffix view_component (3.5.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) @@ -787,14 +534,6 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webfinger (2.1.2) - activesupport - faraday (~> 2.0) - faraday-follow_redirects - webmock (3.19.1) - addressable (>= 2.8.0) - crack (>= 0.3.2) - hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -851,10 +590,7 @@ DEPENDENCIES importmap-rails jbuilder jsbundling-rails - jwt launchy - omniauth-rails_csrf_protection - omniauth_openid_connect pg pry-byebug pry-doc @@ -880,7 +616,6 @@ DEPENDENCIES turbo-rails tzinfo-data web-console - webmock yard-junk RUBY VERSION From 4e82e2cdbc03346029df957155b36eea1247b99f Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 31 Oct 2023 09:24:09 +0000 Subject: [PATCH 010/122] reinstall dependencies --- Gemfile.lock | 280 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 171 insertions(+), 109 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d3a94e790..e9d0bc8eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,78 +1,88 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + actioncable (7.1.1) + actionpack (= 7.1.1) + activesupport (= 7.1.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + zeitwerk (~> 2.6) + actionmailbox (7.1.1) + actionpack (= 7.1.1) + activejob (= 7.1.1) + activerecord (= 7.1.1) + activestorage (= 7.1.1) + activesupport (= 7.1.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8) - actionpack (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activesupport (= 7.0.8) + actionmailer (7.1.1) + actionpack (= 7.1.1) + actionview (= 7.1.1) + activejob (= 7.1.1) + activesupport (= 7.1.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8) - actionview (= 7.0.8) - activesupport (= 7.0.8) - rack (~> 2.0, >= 2.2.4) + rails-dom-testing (~> 2.2) + actionpack (7.1.1) + actionview (= 7.1.1) + activesupport (= 7.1.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8) - actionpack (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.1) + actionpack (= 7.1.1) + activerecord (= 7.1.1) + activestorage (= 7.1.1) + activesupport (= 7.1.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8) - activesupport (= 7.0.8) + actionview (7.1.1) + activesupport (= 7.1.1) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8) - activesupport (= 7.0.8) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.1) + activesupport (= 7.1.1) globalid (>= 0.3.6) - activemodel (7.0.8) - activesupport (= 7.0.8) - activerecord (7.0.8) - activemodel (= 7.0.8) - activesupport (= 7.0.8) - activestorage (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activesupport (= 7.0.8) + activemodel (7.1.1) + activesupport (= 7.1.1) + activerecord (7.1.1) + activemodel (= 7.1.1) + activesupport (= 7.1.1) + timeout (>= 0.4.0) + activestorage (7.1.1) + actionpack (= 7.1.1) + activejob (= 7.1.1) + activerecord (= 7.1.1) + activesupport (= 7.1.1) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8) + activesupport (7.1.1) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) - ahoy_matey (5.0.1) + aes_key_wrap (1.1.0) + ahoy_matey (5.0.2) activesupport (>= 6.1) device_detector (>= 1) safely_block (>= 0.4) ast (2.4.2) + attr_required (1.0.1) backports (3.24.1) base64 (0.1.1) bcrypt (3.1.19) @@ -80,10 +90,12 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) + bigdecimal (3.1.4) + bindata (2.4.15) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.16.0) + bootsnap (1.17.0) msgpack (~> 1.2) brakeman (6.0.1) builder (3.2.4) @@ -99,10 +111,11 @@ GEM xpath (~> 3.2) choice (0.2.0) coderay (1.1.3) - combine_pdf (1.0.23) + combine_pdf (1.0.24) matrix ruby-rc4 (>= 0.1.5) concurrent-ruby (1.2.2) + connection_pool (2.4.1) contentful (2.17.0) http (> 0.8, < 6.0) multi_json (~> 1) @@ -129,7 +142,7 @@ GEM debug_inspector (1.1.0) declarative (0.0.20) device_detector (1.1.1) - devise (4.9.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -146,6 +159,8 @@ GEM dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) + drb (2.1.1) + ruby2_keywords dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) @@ -180,18 +195,20 @@ GEM base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) faraday-net_http (3.0.2) ffi (1.16.3) ffi-compiler (1.0.1) ffi (>= 1.0.0) rake foreman (0.87.2) - fugit (1.8.1) + fugit (1.9.0) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-apis-core (0.11.1) + google-apis-core (0.11.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -259,11 +276,12 @@ GEM i18n (1.14.1) concurrent-ruby (~> 1.0) ice_nine (0.11.2) - importmap-rails (1.2.1) + importmap-rails (1.2.3) actionpack (>= 6.0.0) + activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.6.0) - irb (1.8.1) + irb (1.8.3) rdoc reline (>= 0.3.8) jbuilder (2.11.5) @@ -272,11 +290,17 @@ GEM jsbundling-rails (1.2.1) railties (>= 6.0.0) json (2.6.3) + json-jwt (1.16.3) + activesupport (>= 4.2) + aes_key_wrap + bindata + faraday (~> 2.0) + faraday-follow_redirects jwt (2.7.1) language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - loofah (2.21.3) + loofah (2.21.4) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -288,11 +312,11 @@ GEM matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.5) - mini_portile2 (2.8.4) minitest (5.20.0) msgpack (1.7.2) multi_json (1.15.0) - net-imap (0.4.0) + mutex_m (0.1.2) + net-imap (0.4.3) date net-protocol net-pop (0.1.2) @@ -302,19 +326,33 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.5.9) - nokogiri (1.15.4) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.15.4-aarch64-linux) racc (~> 1.4) - nokogiri (1.15.4-arm64-darwin) - racc (~> 1.4) - nokogiri (1.15.4-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.15.4-x86_64-linux) - racc (~> 1.4) notifications-ruby-client (5.4.0) jwt (>= 1.5, < 3) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + omniauth_openid_connect (0.7.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.2.0) + activemodel + attr_required (>= 1.0.0) + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + net-smtp + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_email + validate_url + webfinger (~> 2.0) orm_adapter (0.5.0) os (1.1.4) pagy (6.1.0) @@ -334,12 +372,12 @@ GEM yard (~> 0.9.11) pry-rails (0.3.9) pry (>= 0.10.4) - psych (5.1.0) + psych (5.1.1.1) stringio public_suffix (5.0.3) puma (6.4.0) nio4r (~> 2.0) - que (2.2.1) + que (2.3.0) que-scheduler (4.4.0) activesupport (>= 5.0) fugit (~> 1.1, >= 1.1.8) @@ -347,23 +385,37 @@ GEM que (>= 0.14, < 3.0.0) raabro (1.4.0) racc (1.7.1) - rack (2.2.8) + rack (3.0.8) + rack-oauth2 (2.2.0) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) + rack-protection (3.0.6) + rack + rack-session (2.0.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8) - actioncable (= 7.0.8) - actionmailbox (= 7.0.8) - actionmailer (= 7.0.8) - actionpack (= 7.0.8) - actiontext (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activemodel (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.1) + actioncable (= 7.1.1) + actionmailbox (= 7.1.1) + actionmailer (= 7.1.1) + actionpack (= 7.1.1) + actiontext (= 7.1.1) + actionview (= 7.1.1) + activejob (= 7.1.1) + activemodel (= 7.1.1) + activerecord (= 7.1.1) + activestorage (= 7.1.1) + activesupport (= 7.1.1) bundler (>= 1.15.0) - railties (= 7.0.8) + railties (= 7.1.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -376,31 +428,32 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) - method_source + railties (7.1.1) + actionpack (= 7.1.1) + activesupport (= 7.1.1) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rdoc (6.5.0) psych (>= 4.0.0) redcarpet (3.6.0) - regexp_parser (2.8.1) + regexp_parser (2.8.2) reline (0.3.9) io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) retriable (3.1.2) rexml (3.2.6) - rouge (4.1.3) + rouge (4.2.0) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -461,14 +514,14 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) safely_block (0.4.0) - selenium-webdriver (4.13.1) + selenium-webdriver (4.14.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.11.0) + sentry-rails (5.12.0) railties (>= 5.0) - sentry-ruby (~> 5.11.0) - sentry-ruby (5.11.0) + sentry-ruby (~> 5.12.0) + sentry-ruby (5.12.0) concurrent-ruby (~> 1.0, >= 1.0.2) show_me_the_cookies (6.0.0) capybara (>= 2, < 4) @@ -493,7 +546,7 @@ GEM slim (5.1.1) temple (~> 0.10.0) tilt (>= 2.1.0) - slim-rails (3.6.2) + slim-rails (3.6.3) actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) @@ -504,15 +557,20 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - stimulus-rails (1.2.2) + stimulus-rails (1.3.0) railties (>= 6.0.0) stringio (3.0.8) + swd (2.0.2) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects temple (0.10.3) - thor (1.2.2) + thor (1.3.0) tilt (2.3.0) timeout (0.4.0) trailblazer-option (0.1.2) - turbo-rails (1.4.0) + turbo-rails (1.5.0) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) @@ -523,6 +581,12 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.5.0) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix view_component (3.5.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) @@ -534,6 +598,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webfinger (2.1.2) + activesupport + faraday (~> 2.0) + faraday-follow_redirects webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -550,15 +618,6 @@ GEM PLATFORMS aarch64-linux-musl - arm64-darwin-20 - arm64-darwin-21 - arm64-darwin-22 - ruby - x86_64-darwin-19 - x86_64-darwin-20 - x86_64-darwin-21 - x86_64-darwin-22 - x86_64-linux DEPENDENCIES ahoy_matey @@ -590,7 +649,10 @@ DEPENDENCIES importmap-rails jbuilder jsbundling-rails + jwt launchy + omniauth-rails_csrf_protection + omniauth_openid_connect pg pry-byebug pry-doc @@ -622,4 +684,4 @@ RUBY VERSION ruby 3.2.2p53 BUNDLED WITH - 2.3.26 + 2.4.10 From 8e0d3630cfe6ece8e85b363f456fff5c5520428b Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 31 Oct 2023 09:29:23 +0000 Subject: [PATCH 011/122] remove gemfile.lock --- Gemfile.lock | 687 --------------------------------------------------- 1 file changed, 687 deletions(-) delete mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index e9d0bc8eb..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,687 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - actioncable (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.1) - actionpack (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activesupport (= 7.1.1) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp - rails-dom-testing (~> 2.2) - actionpack (7.1.1) - actionview (= 7.1.1) - activesupport (= 7.1.1) - nokogiri (>= 1.8.5) - rack (>= 2.2.4) - rack-session (>= 1.0.1) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - actiontext (7.1.1) - actionpack (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) - globalid (>= 0.6.0) - nokogiri (>= 1.8.5) - actionview (7.1.1) - activesupport (= 7.1.1) - builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (7.1.1) - activesupport (= 7.1.1) - globalid (>= 0.3.6) - activemodel (7.1.1) - activesupport (= 7.1.1) - activerecord (7.1.1) - activemodel (= 7.1.1) - activesupport (= 7.1.1) - timeout (>= 0.4.0) - activestorage (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activesupport (= 7.1.1) - marcel (~> 1.0) - activesupport (7.1.1) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - aes_key_wrap (1.1.0) - ahoy_matey (5.0.2) - activesupport (>= 6.1) - device_detector (>= 1) - safely_block (>= 0.4) - ast (2.4.2) - attr_required (1.0.1) - backports (3.24.1) - base64 (0.1.1) - bcrypt (3.1.19) - better_errors (2.10.1) - erubi (>= 1.0.0) - rack (>= 0.9.0) - rouge (>= 1.0.0) - bigdecimal (3.1.4) - bindata (2.4.15) - bindex (0.8.1) - binding_of_caller (1.0.0) - debug_inspector (>= 0.0.1) - bootsnap (1.17.0) - msgpack (~> 1.2) - brakeman (6.0.1) - builder (3.2.4) - byebug (11.1.3) - capybara (3.39.2) - addressable - matrix - mini_mime (>= 0.1.3) - nokogiri (~> 1.8) - rack (>= 1.6.0) - rack-test (>= 0.6.3) - regexp_parser (>= 1.5, < 3.0) - xpath (~> 3.2) - choice (0.2.0) - coderay (1.1.3) - combine_pdf (1.0.24) - matrix - ruby-rc4 (>= 0.1.5) - concurrent-ruby (1.2.2) - connection_pool (2.4.1) - contentful (2.17.0) - http (> 0.8, < 6.0) - multi_json (~> 1) - contentful-management (2.13.1) - http (> 1.0, < 5.0) - json (>= 1.8, < 3.0) - multi_json (~> 1) - contentful_model (1.3.0) - activesupport - contentful (~> 2.7) - contentful-management (~> 2.0) - redcarpet - contentful_rails (0.5.0) - contentful_model (~> 1.0) - rails (>= 4.2) - redcarpet (~> 3.2) - crass (1.0.6) - cssbundling-rails (1.3.3) - railties (>= 6.0.0) - date (3.3.3) - debug (1.8.0) - irb (>= 1.5.0) - reline (>= 0.3.1) - debug_inspector (1.1.0) - declarative (0.0.20) - device_detector (1.1.1) - devise (4.9.3) - bcrypt (~> 3.0) - orm_adapter (~> 0.1) - railties (>= 4.1.0) - responders - warden (~> 1.2.3) - dibber (0.7.0) - diff-lcs (1.5.0) - digest-crc (0.6.5) - rake (>= 12.0.0, < 14.0.0) - docile (1.4.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) - drb (2.1.1) - ruby2_keywords - dry-core (1.0.1) - concurrent-ruby (~> 1.0) - zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-initializer (3.1.1) - dry-logic (1.5.0) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-struct (1.6.0) - dry-core (~> 1.0, < 2) - dry-types (>= 1.7, < 2) - ice_nine (~> 0.11) - zeitwerk (~> 2.6) - dry-types (1.7.1) - concurrent-ruby (~> 1.0) - dry-core (~> 1.0) - dry-inflector (~> 1.0) - dry-logic (~> 1.4) - zeitwerk (~> 2.6) - erubi (1.12.0) - et-orbi (1.2.7) - tzinfo - factory_bot (6.2.1) - activesupport (>= 5.0.0) - factory_bot_rails (6.2.0) - factory_bot (~> 6.2.0) - railties (>= 5.0.0) - faker (3.2.1) - i18n (>= 1.8.11, < 2) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-follow_redirects (0.3.0) - faraday (>= 1, < 3) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake - foreman (0.87.2) - fugit (1.9.0) - et-orbi (~> 1, >= 1.2.7) - raabro (~> 1.4) - globalid (1.2.1) - activesupport (>= 6.1) - google-apis-core (0.11.2) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - govuk-components (4.1.1) - html-attributes-utils (~> 1.0.0, >= 1.0.0) - pagy (~> 6.0) - view_component (>= 3.3, < 3.6) - govuk_design_system_formbuilder (4.1.1) - actionview (>= 6.1) - activemodel (>= 6.1) - activesupport (>= 6.1) - html-attributes-utils (~> 1) - govuk_markdown (2.0.1) - activesupport - redcarpet - govuk_notify_rails (2.2.0) - notifications-ruby-client (~> 5.1) - rails (>= 4.1.0) - grover (1.1.5) - combine_pdf (~> 1.0) - nokogiri (~> 1.0) - hashie (5.0.0) - html-attributes-utils (1.0.2) - activesupport (>= 6.1.4.4) - http (4.4.1) - addressable (~> 2.3) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - http-parser (~> 1.2.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) - http-parser (1.2.3) - ffi-compiler (>= 1.0, < 2.0) - httpclient (2.8.3) - i18n (1.14.1) - concurrent-ruby (~> 1.0) - ice_nine (0.11.2) - importmap-rails (1.2.3) - actionpack (>= 6.0.0) - activesupport (>= 6.0.0) - railties (>= 6.0.0) - io-console (0.6.0) - irb (1.8.3) - rdoc - reline (>= 0.3.8) - jbuilder (2.11.5) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) - jsbundling-rails (1.2.1) - railties (>= 6.0.0) - json (2.6.3) - json-jwt (1.16.3) - activesupport (>= 4.2) - aes_key_wrap - bindata - faraday (~> 2.0) - faraday-follow_redirects - jwt (2.7.1) - language_server-protocol (3.17.0.3) - launchy (2.5.2) - addressable (~> 2.8) - loofah (2.21.4) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.2) - matrix (0.4.2) - method_source (1.0.0) - mini_mime (1.1.5) - minitest (5.20.0) - msgpack (1.7.2) - multi_json (1.15.0) - mutex_m (0.1.2) - net-imap (0.4.3) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.1) - timeout - net-smtp (0.4.0) - net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-aarch64-linux) - racc (~> 1.4) - notifications-ruby-client (5.4.0) - jwt (>= 1.5, < 3) - omniauth (2.1.1) - hashie (>= 3.4.6) - rack (>= 2.2.3) - rack-protection - omniauth-rails_csrf_protection (1.0.1) - actionpack (>= 4.2) - omniauth (~> 2.0) - omniauth_openid_connect (0.7.1) - omniauth (>= 1.9, < 3) - openid_connect (~> 2.2) - openid_connect (2.2.0) - activemodel - attr_required (>= 1.0.0) - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.16) - net-smtp - rack-oauth2 (~> 2.2) - swd (~> 2.0) - tzinfo - validate_email - validate_url - webfinger (~> 2.0) - orm_adapter (0.5.0) - os (1.1.4) - pagy (6.1.0) - parallel (1.23.0) - parser (3.2.2.4) - ast (~> 2.4.1) - racc - pg (1.5.4) - pry (0.14.2) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) - pry-doc (1.4.0) - pry (~> 0.11) - yard (~> 0.9.11) - pry-rails (0.3.9) - pry (>= 0.10.4) - psych (5.1.1.1) - stringio - public_suffix (5.0.3) - puma (6.4.0) - nio4r (~> 2.0) - que (2.3.0) - que-scheduler (4.4.0) - activesupport (>= 5.0) - fugit (~> 1.1, >= 1.1.8) - hashie (>= 3, < 6) - que (>= 0.14, < 3.0.0) - raabro (1.4.0) - racc (1.7.1) - rack (3.0.8) - rack-oauth2 (2.2.0) - activesupport - attr_required - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.11.0) - rack (>= 2.1.0) - rack-protection (3.0.6) - rack - rack-session (2.0.0) - rack (>= 3.0.0) - rack-test (2.1.0) - rack (>= 1.3) - rackup (2.1.0) - rack (>= 3) - webrick (~> 1.8) - rails (7.1.1) - actioncable (= 7.1.1) - actionmailbox (= 7.1.1) - actionmailer (= 7.1.1) - actionpack (= 7.1.1) - actiontext (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activemodel (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) - bundler (>= 1.15.0) - railties (= 7.1.1) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-erd (1.7.2) - activerecord (>= 4.2) - activesupport (>= 4.2) - choice (~> 0.2.0) - ruby-graphviz (~> 1.2) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) - irb - rackup (>= 1.0.0) - rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) - rainbow (3.1.1) - rake (13.1.0) - rdoc (6.5.0) - psych (>= 4.0.0) - redcarpet (3.6.0) - regexp_parser (2.8.2) - reline (0.3.9) - io-console (~> 0.5) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) - retriable (3.1.2) - rexml (3.2.6) - rouge (4.2.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.0.3) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - rspec-support (3.12.1) - rubocop (1.55.0) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.2.2.3) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) - rubocop-capybara (2.19.0) - rubocop (~> 1.41) - rubocop-factory_bot (2.24.0) - rubocop (~> 1.33) - rubocop-govuk (4.12.0) - rubocop (= 1.55.0) - rubocop-ast (= 1.29.0) - rubocop-rails (= 2.20.2) - rubocop-rake (= 0.6.0) - rubocop-rspec (= 2.22.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.20.2) - activesupport (>= 4.2.0) - rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) - rubocop-rake (0.6.0) - rubocop (~> 1.0) - rubocop-rspec (2.22.0) - rubocop (~> 1.33) - rubocop-capybara (~> 2.17) - rubocop-factory_bot (~> 2.22) - ruby-graphviz (1.2.5) - rexml - ruby-progressbar (1.13.0) - ruby-rc4 (0.1.5) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - safely_block (0.4.0) - selenium-webdriver (4.14.0) - rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) - websocket (~> 1.0) - sentry-rails (5.12.0) - railties (>= 5.0) - sentry-ruby (~> 5.12.0) - sentry-ruby (5.12.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - show_me_the_cookies (6.0.0) - capybara (>= 2, < 4) - signet (0.18.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simplecov (0.22.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.4) - site_prism (4.0.3) - addressable (~> 2.8) - capybara (~> 3.27) - site_prism-all_there (~> 2.0) - site_prism-all_there (2.0.2) - sitemap_generator (6.3.0) - builder (~> 3.0) - slim (5.1.1) - temple (~> 0.10.0) - tilt (>= 2.1.0) - slim-rails (3.6.3) - actionpack (>= 3.1) - railties (>= 3.1) - slim (>= 3.0, < 6.0, != 5.0.0) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - stimulus-rails (1.3.0) - railties (>= 6.0.0) - stringio (3.0.8) - swd (2.0.2) - activesupport (>= 3) - attr_required (>= 0.0.5) - faraday (~> 2.0) - faraday-follow_redirects - temple (0.10.3) - thor (1.3.0) - tilt (2.3.0) - timeout (0.4.0) - trailblazer-option (0.1.2) - turbo-rails (1.5.0) - actionpack (>= 6.0.0) - activejob (>= 6.0.0) - railties (>= 6.0.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.5.0) - validate_email (0.1.6) - activemodel (>= 3.0) - mail (>= 2.2.5) - validate_url (1.0.15) - activemodel (>= 3.0.0) - public_suffix - view_component (3.5.0) - activesupport (>= 5.2.0, < 8.0) - concurrent-ruby (~> 1.0) - method_source (~> 1.0) - warden (1.2.9) - rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) - bindex (>= 0.4.0) - railties (>= 6.0.0) - webfinger (2.1.2) - activesupport - faraday (~> 2.0) - faraday-follow_redirects - webrick (1.8.1) - websocket (1.2.10) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - xpath (3.2.0) - nokogiri (~> 1.8) - yard (0.9.34) - yard-junk (0.0.9) - backports (>= 3.18) - rainbow - yard - zeitwerk (2.6.12) - -PLATFORMS - aarch64-linux-musl - -DEPENDENCIES - ahoy_matey - better_errors - binding_of_caller - bootsnap - brakeman - capybara - contentful_rails - cssbundling-rails - debug - devise - dibber - dotenv-rails - dry-core - dry-inflector - dry-initializer - dry-struct - dry-types - factory_bot_rails - faker - foreman - google-cloud-storage - govuk-components - govuk_design_system_formbuilder - govuk_markdown - govuk_notify_rails - grover - importmap-rails - jbuilder - jsbundling-rails - jwt - launchy - omniauth-rails_csrf_protection - omniauth_openid_connect - pg - pry-byebug - pry-doc - pry-rails - puma - que-scheduler - rails - rails-erd - redcarpet - rspec-rails - rubocop-govuk - rubocop-performance - selenium-webdriver - sentry-rails - sentry-ruby - show_me_the_cookies - simplecov - site_prism - sitemap_generator - slim-rails - sprockets-rails - stimulus-rails - turbo-rails - tzinfo-data - web-console - yard-junk - -RUBY VERSION - ruby 3.2.2p53 - -BUNDLED WITH - 2.4.10 From e0af9edc091f26d2a4b88b5d3248ed889072ad10 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 31 Oct 2023 10:55:41 +0000 Subject: [PATCH 012/122] add finding users by gov one id token --- .../users/omniauth_callbacks_controller.rb | 15 ++++++++----- app/helpers/gov_one_helper.rb | 2 +- app/models/user.rb | 4 ++-- .../20231031094611_add_id_token_to_users.rb | 6 ++++++ db/schema.rb | 4 +++- .../omniauth_callbacks_controller_spec.rb | 21 ++++++++++++++----- 6 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20231031094611_add_id_token_to_users.rb diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 7214ae173..816b0d61f 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -10,23 +10,28 @@ def openid_connect end access_token = tokens_response['access_token'] - session[:id_token] = tokens_response['id_token'] user_info_response = auth_service.user_info(access_token) if user_info_response.empty? || user_info_response['email'].blank? flash[:alert] = 'There was a problem signing in. Please try again.' redirect_to root_path and return end - @email = user_info_response['email'] - gov_user = find_or_create_user + gov_user = find_or_create_user(user_info_response['email'], tokens_response['id_token']) sign_in_and_redirect gov_user if gov_user end private # @return [User] - def find_or_create_user - User.find_by(email: @email) || User.create_from_email(@email) + def find_or_create_user(email, id_token) + existing_user = User.find_by(email: email) + + if existing_user + existing_user.update!(id_token: id_token) + else + existing_user = User.find_by(id_token: id_token) || User.create_from_gov_one(email: email, id_token: id_token) + end + existing_user end def after_sign_in_path_for(resource) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index a8eefb2dc..aea147cfe 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -6,6 +6,6 @@ def login_uri # @return [String] def logout_uri - "#{ENV['GOV_ONE_SIGN_OUT_URI']}?id_token_hint=#{session[:id_token]}&post_logout_redirect_uri=#{ENV['GOV_ONE_SIGN_OUT_REDIRECT_URI']}&state=#{SecureRandom.uuid}" + "#{ENV['GOV_ONE_SIGN_OUT_URI']}?id_token_hint=#{current_user.id_token}&post_logout_redirect_uri=#{ENV['GOV_ONE_SIGN_OUT_REDIRECT_URI']}&state=#{SecureRandom.uuid}" end end diff --git a/app/models/user.rb b/app/models/user.rb index 03dfc932f..24d53ab25 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,8 +22,8 @@ def self.dashboard_headers end # @return [User] - def self.create_from_email(email) - user = User.new(email: email, confirmed_at: Time.zone.now) + def self.create_from_gov_one(email:, id_token:) + user = User.new(email: email, id_token: id_token, confirmed_at: Time.zone.now) user.save!(validate: false) user end diff --git a/db/migrate/20231031094611_add_id_token_to_users.rb b/db/migrate/20231031094611_add_id_token_to_users.rb new file mode 100644 index 000000000..47b69f094 --- /dev/null +++ b/db/migrate/20231031094611_add_id_token_to_users.rb @@ -0,0 +1,6 @@ +class AddIdTokenToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :id_token, :string + add_index :users, :id_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index bc371754d..5542f54b2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_08_23_163600) do +ActiveRecord::Schema[7.0].define(version: 2023_10_31_094611) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -229,8 +229,10 @@ t.string "closed_reason_custom" t.boolean "training_emails" t.boolean "early_years_emails" + t.string "id_token" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true + t.index ["id_token"], name: "index_users_on_id_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["unlock_token"], name: "index_users_on_unlock_token" end diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 2d768fef8..87c7ebfd8 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -19,22 +19,33 @@ end context 'when a User does not exist' do - it 'sets session id_token and creates the user' do + it 'creates the user with the email and id_token' do get :openid_connect, params: { 'code' => 'mock_code' } - expect(session[:id_token]).to eq(id_token) expect(User.find_by(email: email)).to be_truthy + expect(User.find_by(id_token: id_token)).to be_truthy expect(response).to redirect_to(edit_registration_name_path) end end - context 'when a User exists' do + context 'when a User email exists' do before do create :user, :registered, email: email end - it 'sets session id_token and signs in the user' do + it 'updates the user id_token and signs them in' do + get :openid_connect, params: { 'code' => 'mock_code' } + expect(User.find_by(id_token: id_token)).to be_truthy + expect(response).to redirect_to(my_modules_path) + end + end + + context 'when a User id_token exists' do + before do + create :user, :registered, id_token: id_token + end + + it 'signs the user in' do get :openid_connect, params: { 'code' => 'mock_code' } - expect(session[:id_token]).to eq(id_token) expect(response).to redirect_to(my_modules_path) end end From 426f64865e8d5a5b6f82e733082b817200e63e76 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 31 Oct 2023 13:39:24 +0000 Subject: [PATCH 013/122] verify gov one redirect state and decode id token --- .../users/omniauth_callbacks_controller.rb | 55 +++++++++++++------ app/helpers/gov_one_helper.rb | 7 ++- app/models/user.rb | 6 +- app/services/gov_one_auth_service.rb | 18 ++++++ ..._rename_id_token_to_gov_one_id_in_users.rb | 5 ++ db/schema.rb | 6 +- .../omniauth_callbacks_controller_spec.rb | 17 ++++-- 7 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 db/migrate/20231031132907_rename_id_token_to_gov_one_id_in_users.rb diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 816b0d61f..12eb552dc 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,35 +1,58 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def openid_connect - code = params['code'] + return error_redirect unless valid_params? - auth_service = GovOneAuthService.new(code) + session.delete(:gov_one_auth_state) + + auth_service = GovOneAuthService.new(params['code']) tokens_response = auth_service.tokens - if tokens_response.empty? || tokens_response['access_token'].blank? || tokens_response['id_token'].blank? - flash[:alert] = 'There was a problem signing in. Please try again.' - redirect_to root_path and return - end - access_token = tokens_response['access_token'] + return error_redirect unless valid_tokens?(tokens_response) - user_info_response = auth_service.user_info(access_token) - if user_info_response.empty? || user_info_response['email'].blank? - flash[:alert] = 'There was a problem signing in. Please try again.' - redirect_to root_path and return - end - gov_user = find_or_create_user(user_info_response['email'], tokens_response['id_token']) + id_token = tokens_response['id_token'] + session[:id_token] = id_token + gov_one_id = auth_service.decode_id_token(id_token)[0]['sub'] + + user_info_response = auth_service.user_info(tokens_response['access_token']) + return error_redirect unless valid_user_info?(user_info_response) + + gov_user = find_or_create_user(user_info_response['email'], gov_one_id) sign_in_and_redirect gov_user if gov_user end private + # @return [Boolean] + def valid_params? + params['code'].present? && params['state'].present? && params['state'] == session[:gov_one_auth_state] + end + + # @param tokens_response [Hash] + # @return [Boolean] + def valid_tokens?(tokens_response) + tokens_response.present? && tokens_response['access_token'].present? && tokens_response['id_token'].present? + end + + # @param user_info_response [Hash] + # @return [Boolean] + def valid_user_info?(user_info_response) + user_info_response.present? && user_info_response['email'].present? + end + + # @return [void] + def error_redirect + flash[:alert] = 'There was a problem signing in. Please try again.' + redirect_to root_path + end + # @return [User] - def find_or_create_user(email, id_token) + def find_or_create_user(email, id) existing_user = User.find_by(email: email) if existing_user - existing_user.update!(id_token: id_token) + existing_user.update!(gov_one_id: id) else - existing_user = User.find_by(id_token: id_token) || User.create_from_gov_one(email: email, id_token: id_token) + existing_user = User.find_by(gov_one_id: id) || User.create_from_gov_one(email: email, gov_one_id: id) end existing_user end diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index aea147cfe..afdc3da79 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -1,11 +1,14 @@ module GovOneHelper # @return [String] def login_uri - "#{ENV['GOV_ONE_AUTH_URI']}&nonce=#{SecureRandom.uuid}&state=#{SecureRandom.uuid}" + state = SecureRandom.uuid + nonce = SecureRandom.uuid + session[:gov_one_auth_state] = state + "#{ENV['GOV_ONE_AUTH_URI']}&nonce=#{nonce}&state=#{state}" end # @return [String] def logout_uri - "#{ENV['GOV_ONE_SIGN_OUT_URI']}?id_token_hint=#{current_user.id_token}&post_logout_redirect_uri=#{ENV['GOV_ONE_SIGN_OUT_REDIRECT_URI']}&state=#{SecureRandom.uuid}" + "#{ENV['GOV_ONE_SIGN_OUT_URI']}?id_token_hint=#{session[:id_token]}&post_logout_redirect_uri=#{ENV['GOV_ONE_SIGN_OUT_REDIRECT_URI']}&state=#{SecureRandom.uuid}" end end diff --git a/app/models/user.rb b/app/models/user.rb index 24d53ab25..72aef162b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,9 +21,11 @@ def self.dashboard_headers DASHBOARD_ATTRS + Training::Module.live.map { |mod| "module_#{mod.position}_time" } end + # @param email [String] + # @param gov_one_id [String] # @return [User] - def self.create_from_gov_one(email:, id_token:) - user = User.new(email: email, id_token: id_token, confirmed_at: Time.zone.now) + def self.create_from_gov_one(email:, gov_one_id:) + user = User.new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) user.save!(validate: false) user end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 14ba31376..446449d2c 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -42,6 +42,16 @@ def user_info(access_token) {} end + # @param token [String] + # @return [Array<Hash>] + def decode_id_token(token) + kid = JWT.decode(token, nil, false).last['kid'] + key_params = jwks['keys'].find { |key| key['kid'] == kid } + jwk = JWT::JWK.new(key_params) + + JWT.decode(token, jwk.public_key, true, algorithm: 'ES256') + end + private # @param uri [URI] @@ -52,6 +62,14 @@ def build_http(uri) http end + def jwks + discovery_url = "#{ENV['GOV_ONE_BASE_URI']}/.well-known/jwks.json" + uri = URI.parse(discovery_url) + http = build_http(uri) + response = http.request(Net::HTTP::Get.new(uri.path)) + JSON.parse(response.body) + end + # @return [String] def jwt_assertion rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) diff --git a/db/migrate/20231031132907_rename_id_token_to_gov_one_id_in_users.rb b/db/migrate/20231031132907_rename_id_token_to_gov_one_id_in_users.rb new file mode 100644 index 000000000..433bedd3c --- /dev/null +++ b/db/migrate/20231031132907_rename_id_token_to_gov_one_id_in_users.rb @@ -0,0 +1,5 @@ +class RenameIdTokenToGovOneIdInUsers < ActiveRecord::Migration[7.0] + def change + rename_column :users, :id_token, :gov_one_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 5542f54b2..166d883ee 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_31_094611) do +ActiveRecord::Schema[7.0].define(version: 2023_10_31_132907) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -229,10 +229,10 @@ t.string "closed_reason_custom" t.boolean "training_emails" t.boolean "early_years_emails" - t.string "id_token" + t.string "gov_one_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true - t.index ["id_token"], name: "index_users_on_id_token", unique: true + t.index ["gov_one_id"], name: "index_users_on_gov_one_id", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["unlock_token"], name: "index_users_on_unlock_token" end diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 87c7ebfd8..c28512af5 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -16,14 +16,17 @@ allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') + allow(auth_service).to receive(:decode_id_token).and_return([{ 'sub' => 'mock_sub' }]) + session[:gov_one_auth_state] = 'mock_state' end context 'when a User does not exist' do it 'creates the user with the email and id_token' do - get :openid_connect, params: { 'code' => 'mock_code' } + get :openid_connect, params: { 'code' => 'mock_code', 'state' => 'mock_state' } expect(User.find_by(email: email)).to be_truthy - expect(User.find_by(id_token: id_token)).to be_truthy + expect(User.find_by(gov_one_id: 'mock_sub')).to be_truthy expect(response).to redirect_to(edit_registration_name_path) + expect(session[:id_token]).to eq(id_token) end end @@ -33,20 +36,22 @@ end it 'updates the user id_token and signs them in' do - get :openid_connect, params: { 'code' => 'mock_code' } - expect(User.find_by(id_token: id_token)).to be_truthy + get :openid_connect, params: { 'code' => 'mock_code', 'state' => 'mock_state' } + expect(User.find_by(gov_one_id: 'mock_sub')).to be_truthy expect(response).to redirect_to(my_modules_path) + expect(session[:id_token]).to eq(id_token) end end context 'when a User id_token exists' do before do - create :user, :registered, id_token: id_token + create :user, :registered, gov_one_id: 'mock_sub' end it 'signs the user in' do - get :openid_connect, params: { 'code' => 'mock_code' } + get :openid_connect, params: { 'code' => 'mock_code', 'state' => 'mock_state' } expect(response).to redirect_to(my_modules_path) + expect(session[:id_token]).to eq(id_token) end end end From 93f7080eb9080c95cca78742fe0aad08bf5603f9 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 1 Nov 2023 09:15:36 +0000 Subject: [PATCH 014/122] update gemfile.lock --- Gemfile.lock | 686 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 686 insertions(+) create mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..e2fab4ca5 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,686 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.8) + actionpack (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activesupport (= 7.0.8) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.8) + actionview (= 7.0.8) + activesupport (= 7.0.8) + rack (~> 2.0, >= 2.2.4) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.8) + actionpack (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.8) + activesupport (= 7.0.8) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.8) + activesupport (= 7.0.8) + globalid (>= 0.3.6) + activemodel (7.0.8) + activesupport (= 7.0.8) + activerecord (7.0.8) + activemodel (= 7.0.8) + activesupport (= 7.0.8) + activestorage (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activesupport (= 7.0.8) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.8) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) + aes_key_wrap (1.1.0) + ahoy_matey (5.0.1) + activesupport (>= 6.1) + device_detector (>= 1) + safely_block (>= 0.4) + ast (2.4.2) + attr_required (1.0.1) + backports (3.24.1) + base64 (0.1.1) + bcrypt (3.1.19) + better_errors (2.10.1) + erubi (>= 1.0.0) + rack (>= 0.9.0) + rouge (>= 1.0.0) + bindata (2.4.15) + bindex (0.8.1) + binding_of_caller (1.0.0) + debug_inspector (>= 0.0.1) + bootsnap (1.16.0) + msgpack (~> 1.2) + brakeman (6.0.1) + builder (3.2.4) + byebug (11.1.3) + capybara (3.39.2) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + choice (0.2.0) + coderay (1.1.3) + combine_pdf (1.0.23) + matrix + ruby-rc4 (>= 0.1.5) + concurrent-ruby (1.2.2) + contentful (2.17.0) + http (> 0.8, < 6.0) + multi_json (~> 1) + contentful-management (2.13.1) + http (> 1.0, < 5.0) + json (>= 1.8, < 3.0) + multi_json (~> 1) + contentful_model (1.3.0) + activesupport + contentful (~> 2.7) + contentful-management (~> 2.0) + redcarpet + contentful_rails (0.5.0) + contentful_model (~> 1.0) + rails (>= 4.2) + redcarpet (~> 3.2) + crass (1.0.6) + cssbundling-rails (1.3.3) + railties (>= 6.0.0) + date (3.3.3) + debug (1.8.0) + irb (>= 1.5.0) + reline (>= 0.3.1) + debug_inspector (1.1.0) + declarative (0.0.20) + device_detector (1.1.1) + devise (4.9.2) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + dibber (0.7.0) + diff-lcs (1.5.0) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + docile (1.4.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-initializer (3.1.1) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-struct (1.6.0) + dry-core (~> 1.0, < 2) + dry-types (>= 1.7, < 2) + ice_nine (~> 0.11) + zeitwerk (~> 2.6) + dry-types (1.7.1) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + erubi (1.12.0) + et-orbi (1.2.7) + tzinfo + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) + faker (3.2.1) + i18n (>= 1.8.11, < 2) + faraday (2.7.11) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-net_http (3.0.2) + ffi (1.16.3) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake + foreman (0.87.2) + fugit (1.8.1) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) + globalid (1.2.1) + activesupport (>= 6.1) + google-apis-core (0.11.1) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + govuk-components (4.1.1) + html-attributes-utils (~> 1.0.0, >= 1.0.0) + pagy (~> 6.0) + view_component (>= 3.3, < 3.6) + govuk_design_system_formbuilder (4.1.1) + actionview (>= 6.1) + activemodel (>= 6.1) + activesupport (>= 6.1) + html-attributes-utils (~> 1) + govuk_markdown (2.0.1) + activesupport + redcarpet + govuk_notify_rails (2.2.0) + notifications-ruby-client (~> 5.1) + rails (>= 4.1.0) + grover (1.1.5) + combine_pdf (~> 1.0) + nokogiri (~> 1.0) + hashie (5.0.0) + html-attributes-utils (1.0.2) + activesupport (>= 6.1.4.4) + http (4.4.1) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 2.2) + http-parser (~> 1.2.0) + http-cookie (1.0.5) + domain_name (~> 0.5) + http-form_data (2.3.0) + http-parser (1.2.3) + ffi-compiler (>= 1.0, < 2.0) + httpclient (2.8.3) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + importmap-rails (1.2.1) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.6.0) + irb (1.8.1) + rdoc + reline (>= 0.3.8) + jbuilder (2.11.5) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + jsbundling-rails (1.2.1) + railties (>= 6.0.0) + json (2.6.3) + json-jwt (1.16.3) + activesupport (>= 4.2) + aes_key_wrap + bindata + faraday (~> 2.0) + faraday-follow_redirects + jwt (2.7.1) + language_server-protocol (3.17.0.3) + launchy (2.5.2) + addressable (~> 2.8) + loofah (2.21.3) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + matrix (0.4.2) + method_source (1.0.0) + mini_mime (1.1.5) + mini_portile2 (2.8.4) + minitest (5.20.0) + msgpack (1.7.2) + multi_json (1.15.0) + net-imap (0.4.0) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.4.0) + net-protocol + nio4r (2.5.9) + nokogiri (1.15.4) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.15.4-aarch64-linux) + racc (~> 1.4) + nokogiri (1.15.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.15.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.15.4-x86_64-linux) + racc (~> 1.4) + notifications-ruby-client (5.4.0) + jwt (>= 1.5, < 3) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + omniauth_openid_connect (0.7.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.2.0) + activemodel + attr_required (>= 1.0.0) + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + net-smtp + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_email + validate_url + webfinger (~> 2.0) + orm_adapter (0.5.0) + os (1.1.4) + pagy (6.1.0) + parallel (1.23.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc + pg (1.5.4) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.10.1) + byebug (~> 11.0) + pry (>= 0.13, < 0.15) + pry-doc (1.4.0) + pry (~> 0.11) + yard (~> 0.9.11) + pry-rails (0.3.9) + pry (>= 0.10.4) + psych (5.1.0) + stringio + public_suffix (5.0.3) + puma (6.4.0) + nio4r (~> 2.0) + que (2.2.1) + que-scheduler (4.4.0) + activesupport (>= 5.0) + fugit (~> 1.1, >= 1.1.8) + hashie (>= 3, < 6) + que (>= 0.14, < 3.0.0) + raabro (1.4.0) + racc (1.7.1) + rack (2.2.8) + rack-oauth2 (2.2.0) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) + rack-test (2.1.0) + rack (>= 1.3) + rails (7.0.8) + actioncable (= 7.0.8) + actionmailbox (= 7.0.8) + actionmailer (= 7.0.8) + actionpack (= 7.0.8) + actiontext (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activemodel (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) + bundler (>= 1.15.0) + railties (= 7.0.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-erd (1.7.2) + activerecord (>= 4.2) + activesupport (>= 4.2) + choice (~> 0.2.0) + ruby-graphviz (~> 1.2) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rainbow (3.1.1) + rake (13.0.6) + rdoc (6.5.0) + psych (>= 4.0.0) + redcarpet (3.6.0) + regexp_parser (2.8.1) + reline (0.3.9) + io-console (~> 0.5) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) + retriable (3.1.2) + rexml (3.2.6) + rouge (4.1.3) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (6.0.3) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) + rubocop (1.55.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.3) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.19.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.24.0) + rubocop (~> 1.33) + rubocop-govuk (4.12.0) + rubocop (= 1.55.0) + rubocop-ast (= 1.29.0) + rubocop-rails (= 2.20.2) + rubocop-rake (= 0.6.0) + rubocop-rspec (= 2.22.0) + rubocop-performance (1.19.1) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.20.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (2.22.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-graphviz (1.2.5) + rexml + ruby-progressbar (1.13.0) + ruby-rc4 (0.1.5) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + safely_block (0.4.0) + selenium-webdriver (4.13.1) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sentry-rails (5.11.0) + railties (>= 5.0) + sentry-ruby (~> 5.11.0) + sentry-ruby (5.11.0) + concurrent-ruby (~> 1.0, >= 1.0.2) + show_me_the_cookies (6.0.0) + capybara (>= 2, < 4) + signet (0.18.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + site_prism (4.0.3) + addressable (~> 2.8) + capybara (~> 3.27) + site_prism-all_there (~> 2.0) + site_prism-all_there (2.0.2) + sitemap_generator (6.3.0) + builder (~> 3.0) + slim (5.1.1) + temple (~> 0.10.0) + tilt (>= 2.1.0) + slim-rails (3.6.2) + actionpack (>= 3.1) + railties (>= 3.1) + slim (>= 3.0, < 6.0, != 5.0.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stimulus-rails (1.2.2) + railties (>= 6.0.0) + stringio (3.0.8) + swd (2.0.2) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects + temple (0.10.3) + thor (1.2.2) + tilt (2.3.0) + timeout (0.4.0) + trailblazer-option (0.1.2) + turbo-rails (1.4.0) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) + unicode-display_width (2.5.0) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix + view_component (3.5.0) + activesupport (>= 5.2.0, < 8.0) + concurrent-ruby (~> 1.0) + method_source (~> 1.0) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webfinger (2.1.2) + activesupport + faraday (~> 2.0) + faraday-follow_redirects + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + yard (0.9.34) + yard-junk (0.0.9) + backports (>= 3.18) + rainbow + yard + zeitwerk (2.6.12) + +PLATFORMS + aarch64-linux-musl + arm64-darwin-20 + arm64-darwin-21 + arm64-darwin-22 + ruby + x86_64-darwin-19 + x86_64-darwin-20 + x86_64-darwin-21 + x86_64-darwin-22 + x86_64-linux + +DEPENDENCIES + ahoy_matey + better_errors + binding_of_caller + bootsnap + brakeman + capybara + contentful_rails + cssbundling-rails + debug + devise + dibber + dotenv-rails + dry-core + dry-inflector + dry-initializer + dry-struct + dry-types + factory_bot_rails + faker + foreman + google-cloud-storage + govuk-components + govuk_design_system_formbuilder + govuk_markdown + govuk_notify_rails + grover + importmap-rails + jbuilder + jsbundling-rails + jwt + launchy + omniauth-rails_csrf_protection + omniauth_openid_connect + pg + pry-byebug + pry-doc + pry-rails + puma + que-scheduler + rails + rails-erd + redcarpet + rspec-rails + rubocop-govuk + rubocop-performance + selenium-webdriver + sentry-rails + sentry-ruby + show_me_the_cookies + simplecov + site_prism + sitemap_generator + slim-rails + sprockets-rails + stimulus-rails + turbo-rails + tzinfo-data + web-console + yard-junk + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.3.26 From 48f4ae55aa6a2b473b80efdb367a34189d887d8c Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 1 Nov 2023 09:26:46 +0000 Subject: [PATCH 015/122] update migration number --- db/migrate/20231031094611_add_id_token_to_users.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20231031094611_add_id_token_to_users.rb b/db/migrate/20231031094611_add_id_token_to_users.rb index 47b69f094..1423064b7 100644 --- a/db/migrate/20231031094611_add_id_token_to_users.rb +++ b/db/migrate/20231031094611_add_id_token_to_users.rb @@ -1,4 +1,4 @@ -class AddIdTokenToUsers < ActiveRecord::Migration[7.1] +class AddIdTokenToUsers < ActiveRecord::Migration[7.0] def change add_column :users, :id_token, :string add_index :users, :id_token, unique: true From 7313839ff0455bbdda6aa8c474202632476b98ec Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 1 Nov 2023 10:55:14 +0000 Subject: [PATCH 016/122] update localisations and add all text to cms --- .gitignore | 4 ---- app/models/user.rb | 2 +- app/services/gov_one_auth_service.rb | 8 ++++++-- app/views/gov_one/info.html.slim | 7 ++----- config/locales/en.yml | 2 ++ 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ba33dc4ad..8ca8af16b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,10 +57,6 @@ localhost.crt localhost.key nscacert.pem -# gov one login keys -private_key.pem -public_key.pem - # Yarn files .yarn/* diff --git a/app/models/user.rb b/app/models/user.rb index 72aef162b..d374a1f63 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,7 +25,7 @@ def self.dashboard_headers # @param gov_one_id [String] # @return [User] def self.create_from_gov_one(email:, gov_one_id:) - user = User.new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) + user = new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) user.save!(validate: false) user end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 446449d2c..e5e9dfaae 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -13,8 +13,7 @@ def tokens client_assertion_type: ENV['GOV_ONE_CLIENT_ASSERTION_TYPE'], client_assertion: jwt_assertion, } - - token_uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") + http = build_http(token_uri) token_request = Net::HTTP::Post.new(token_uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) @@ -54,6 +53,11 @@ def decode_id_token(token) private + # @return [URI] + def token_uri + URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") + end + # @param uri [URI] # @return [Net::HTTP] def build_http(uri) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index 611b35e4f..da7a2602f 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -3,15 +3,12 @@ - content_for :page_title do = html_title t('gov-one.info.title') -.h1.govuk-heading-xl - = t('gov-one.info.title') - .govuk-grid-row .govuk-grid-column-one-half = m('gov-one-info.body') - if current_user - = govuk_button_link_to 'Sign out of Gov One Login', logout_uri + = govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri - else - = govuk_button_link_to 'Sign in with Gov One Login', login_uri + = govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index d8c8028b0..69b7c3d60 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -566,6 +566,8 @@ en: gov-one: info: title: Gov One Login + sign-in-button: Sign in with Gov One Login + sign-out-button: Sign out of Gov One Login # /settings/cookie-policy cookie_policy: From 5cf8cc62ed906b2556a57c2bfdb76f4aa1abbf11 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 1 Nov 2023 11:07:48 +0000 Subject: [PATCH 017/122] update localisations --- config/locales/en.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 69b7c3d60..463dc2a39 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -566,6 +566,10 @@ en: gov-one: info: title: Gov One Login + body: | + # Gov One Login + + This is some information about Gov One Login... sign-in-button: Sign in with Gov One Login sign-out-button: Sign out of Gov One Login From 66170decebdc3c1824e326e0d42d1283970580d6 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 1 Nov 2023 11:09:06 +0000 Subject: [PATCH 018/122] rubocop --- app/services/gov_one_auth_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index e5e9dfaae..80dc7757c 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -13,7 +13,7 @@ def tokens client_assertion_type: ENV['GOV_ONE_CLIENT_ASSERTION_TYPE'], client_assertion: jwt_assertion, } - + http = build_http(token_uri) token_request = Net::HTTP::Post.new(token_uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) From 51b8acd33018b7d25cf0daeb1640f6b9691ce22a Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 1 Nov 2023 12:56:25 +0000 Subject: [PATCH 019/122] update omniauth callbacks controller --- app/controllers/users/omniauth_callbacks_controller.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 12eb552dc..75df54064 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -48,15 +48,18 @@ def error_redirect # @return [User] def find_or_create_user(email, id) existing_user = User.find_by(email: email) - - if existing_user + if User.find_by(email: email) existing_user.update!(gov_one_id: id) + elsif User.find_by(gov_one_id: id) + existing_user = User.find_by(gov_one_id: id) + existing_user.update!(email: email) else - existing_user = User.find_by(gov_one_id: id) || User.create_from_gov_one(email: email, gov_one_id: id) + return User.create_from_gov_one(email: email, gov_one_id: id) end existing_user end + # @return [String] def after_sign_in_path_for(resource) if resource.registration_complete? if resource.display_whats_new? From 70816b5307a89d8b3ae40d6ccb0a626ea8cccd94 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 2 Nov 2023 09:30:51 +0000 Subject: [PATCH 020/122] update credentials --- app/helpers/gov_one_helper.rb | 23 +++++++++++++++++++--- app/services/gov_one_auth_service.rb | 11 +++++++---- config/application.rb | 2 ++ config/credentials/development.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/test.yml.enc | 2 +- config/initializers/devise.rb | 2 +- spec/services/gov_one_auth_service_spec.rb | 2 -- 8 files changed, 33 insertions(+), 13 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index afdc3da79..58530c977 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -2,13 +2,30 @@ module GovOneHelper # @return [String] def login_uri state = SecureRandom.uuid - nonce = SecureRandom.uuid session[:gov_one_auth_state] = state - "#{ENV['GOV_ONE_AUTH_URI']}&nonce=#{nonce}&state=#{state}" + + params = { + response_type: 'code', + scope: 'email openid', + client_id: Rails.application.config.gov_one_client_id, + nonce: SecureRandom.uuid, + state: state, + } + + uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/authorize") + uri.query = URI.encode_www_form(params) + "#{uri.to_s}&redirect_uri=#{ENV['GOV_ONE_REDIRECT_URI']}" end # @return [String] def logout_uri - "#{ENV['GOV_ONE_SIGN_OUT_URI']}?id_token_hint=#{session[:id_token]}&post_logout_redirect_uri=#{ENV['GOV_ONE_SIGN_OUT_REDIRECT_URI']}&state=#{SecureRandom.uuid}" + params = { + id_token_hint: session[:id_token], + state: SecureRandom.uuid, + } + + uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/logout") + uri.query = URI.encode_www_form(params) + "#{uri.to_s}&post_logout_redirect_uri=#{ENV['GOV_ONE_LOGOUT_REDIRECT_URI']}" end end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 80dc7757c..46521d504 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -10,7 +10,7 @@ def tokens grant_type: 'authorization_code', code: @code, redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], - client_assertion_type: ENV['GOV_ONE_CLIENT_ASSERTION_TYPE'], + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", client_assertion: jwt_assertion, } @@ -29,7 +29,6 @@ def tokens # @param access_token [String] # @return [Hash] def user_info(access_token) - userinfo_uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/userinfo") http = build_http(userinfo_uri) userinfo_request = Net::HTTP::Get.new(userinfo_uri.path, { 'Authorization' => "Bearer #{access_token}" }) @@ -58,6 +57,10 @@ def token_uri URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") end + def userinfo_uri + URI.parse("#{ENV['GOV_ONE_BASE_URI']}/userinfo") + end + # @param uri [URI] # @return [Net::HTTP] def build_http(uri) @@ -80,8 +83,8 @@ def jwt_assertion payload = { aud: "#{ENV['GOV_ONE_BASE_URI']}/token", - iss: ENV['GOV_ONE_CLIENT_ID'], - sub: ENV['GOV_ONE_CLIENT_ID'], + iss: Rails.application.config.gov_one_client_id, + sub: Rails.application.config.gov_one_client_id, exp: Time.zone.now.to_i + 5 * 60, jti: SecureRandom.uuid, iat: Time.zone.now.to_i, diff --git a/config/application.rb b/config/application.rb index 10db7302e..90a2dc030 100644 --- a/config/application.rb +++ b/config/application.rb @@ -68,7 +68,9 @@ class Application < Rails::Application config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) # Gov one + config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI') config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) + config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) # @return [Boolean] def live? diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index b4fe3c9ae..4606457df 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -Y+EgIqYzaAxckrOE38ArevcqOV3rUrkTDRDDPzNBJnT87ckbDtlB2/Uhyvv/n29dvTRtAX6eQXAflgZL1EmGuZgyTZoE42/nAgYJ3RxER5ALL+Qa8t9oxiZTRT4ujbe7lmw6R9dmUOV/zMaYnxKT6OczkmWmipyJoUxTtmeoQKwnee7JX8rn5g5Umpu+dWFiA9JXxiAWOlyu+fRWyLObTceE07dHlwEXioTQvzq+4xnhNQ8Tx3SOZhSQXSKM1awr/j5snKdW1T56A9CybOhkzTV6FKa+H1i4YteHUfAXUd2JfDahynNOkQ/bfMP3y2hNhKgmaWum8PDNqlBo+WSNyYfmilpxeuSCIFNtFFvL3uus9/lFlQUqkiTw0wqHWK+FawGXcViakgcix9Vc+nLOlVfWvHgzGZ0KS0BUfKZ0k+hP/xIhr+FgkXR/PSSpC48f192DFqcZX72yQqTiRxtEVqEOEhsrY80DQPMZ0kcMho8s0uLQuz87qj5z2uS1VP+S/eVNw9D6F8W3FcCVw5UrNuJ+p3K/HA04cMLoQvIEtiMH3eUQJBlYdwxuRAdJmBuykr4vZo1eUu4t+vzRgSGHVLuWrlRSZS0FcJzJZkVyTmaf5iVm+jlMf1u9pQCFCQRycAePVW+MRJZ1lS3rbbcBvdtaSZT2ybRjvhHsfROp/H4/E0PBL2q13pBcLDaaHgXvJtjV/lKaFhm8XIDw5P8pmmzC5YXQ4k48n8zZwJcXBW8mAouvZBA9Lqcsjro0JnKFd+FJkvgNdq3pVcnYLXVRwsvy60HqKLbfCUW1XAYq4i/OQ+doUWShSuBeTdeMf34uuZ43fQaySXgloD1SUjbCx1tqEsZKxkQhRgxOkowS1fyYqV7OuvTKIZke23teWhOPR1I+nkLzUGO6l7HONrKZSC42/0jtakQKRknIROi+/vvkFP49Y5cQo3+d+HhPIdQrB+/HQluTgnTBjky9SVjGTFG9W700pXSxcdL0RzDjqd9yQSXURhFkm6A4HOrM20nitacVF+FaLF06MxCNwuwnUy5/NMDLawginnMQCScnNzfCxayXO7mwmYELowAdClonf8YFpejnKcSbMLcijTgW17ihh88ItjmLK1y4DBuUZ93AyD8rx8uWdTrEpQBCWehnCv6YzxoS2BLZqLPM1UUZjyAVQE/t4Mk7QOCuLyND7f8DVxPikxD0UH16wXOA2DSMOcxf/LfqKOlLsTedx2pEzzD+SJK9UeUED2OJh4aLzX8btuguiZQ+7DPvYwciG3lBktTUOiP5rCX4UxhDra9/Muw5DOKBqLIqoCn7AfXFuOPy+HpRGT+M8l/e3RpSMR6AO+5OERGpqNSRc+uuIzEMsw/JosyFXv+uyqQVksjwMYi25h9HGtAyEPNnDfluCifHAVY/zB0065kdN2xxorlrhXXico0/yVu6Z5M+KVpkKj2shqsCjCWY1iWy7EyGxi/TYw/JIspIlK+2xOLbbQfNytfNRU1I3TxXlEyc9IwOneqb3weZninvGMXym5t40NIudabTlTnKjlivissXRT3qdI80uY3L1I4aIIBqovkNXJAYrY7lBvczjqWenlu69xiAkQDjVslLBZPwlLZZB4lzIM1NgJWVPV+EqNM/rUD5uUhrDP8Cjo6JJ6khTgq3PhwbdmfBxwYFUJhUNbUQpa+F+YKj1xR+HaCIMETh48pOwpl4gMEqhaNb/XKsET0tRPYMaOCdw8GNgBTuyK4sYuFYwJHB0xXI8URnN3EkgwfSwbUW4zCxb/B1rJ3t1HK4+ou0eaEMtIPaqcqFEzdZtyF/cYlXyZZQFTXTqYaUoS6Ct3dSA2O5uRpjkDh4zInx2Bkxa+kODL0boaRjgWXb5i6DgvswszHig/ZYhTIZJ8zZLjZsOkcZzhU72O/prFeeqeocp4t9evYSIrsm5436pA1eRFhEcqkRA+zwhKjtQXSFRNBxh0ahBlWSHPY8PnYF+0nGnrNI1tDnKrVgJ2dHEBBv4dcB7FExlRf7VhkZOdYsnJcGuy3hMyHo7bVx8dtgK28tbgZAE6E8AfnrMCmgn8sgopW/Ds0Ne5zgFojoIfMtO4WTWCPUlELu3+qO/8j3Ig+V30IXayBY6KPS0i1OStnw9x8hrNfyXr425Z4hJMyGcd9ux89f7ulhiGh8BaXuEK1Iyktww8zW+2ipUZ28IblQ80x1ywXPSE2pvtiSDDV7EV8wjM+7qnsXgdoK8jTAw3H5aInXZeW+WHNH8H2Fu1RdeMUnXY3HNb5MkY+NTpEutu2ewiGzNkQIXoqGYO8TIdnjGphJE5Ui56K7v5kikuhk00KFN36Ts1oBBBUpCBUT3Om9qbnp4H4VxxnTERyJcE1iHeMjSxABnxhyPmJhOxIVXIr9MJRkWj3A/JgUEXFQASZIVXSJmxSljuhEu3Gck3ZCy9fD3pB3OzUt6AKSB1x6/HazjruYkoXpuLpybClf/uthj4LvPK5HlnkR2sUFjFwU5aSxTvuVVBQe8XWHa7Mp6zx5G2Ri2M0dQ+b3dBuMI1tvVNZw3t0z6FTtNzvyW2wM9ewg3LsDoqi7d8Gab2faOGTJTARabCiKJFBEnJL0X8mOWjnvP0+G1sVsfLvlTHkrbCl8d3udlzRuBSvxaH+NkkqUDFKmPqUpUff9jPrC2sgUDx8B8lhpG1AJ7MKEXncEdnM299d4+RC/sEGLeMlQCwJujQ9IptxDPX9JD04ZAuHRNGakpJqlD1nsQwmC2d6nJpP9DiXhp5T+kK7i7cmWhuIi5Bs9EeeotBN+V2F/atyjzFafQYmLgSv+EMGSdpiadvosugWFdyZrk+7wCIeU1md4LfEFK+FKhZdBaWVcMkvAncnYkPa6Xtalmt8RshRKndRYrgD6OZ3RE6p7HfPElhvqi1cN7vg+tGt2dzNSmCQWMe6MtogRZdGy4g/pl24f+Dc76e3FG2vF/x8yeFsxpK5gPw0nQ0mQvgamXjEAOiSjs8TP62NEzvP9idnHbwPNZ/yGMoToiZoKnWV2GPiL2KaLYjpE69vxotmUZgPuDUd2TtzvgXreeXR4JilgNXq+E9UyeQDZCgjEyXw054taxnxy32CKars/8Z0CFjxXbHztGOUDjeInwxZSzCIzEibMkffvMPGFeP9lts6FUsa3oOmJCG3NFAJ/ayyYSths+FzPAHAKkx6PN458wbJt0BylLR5qqvV8otofPSwVs6b1jf6cwxmp8O5iPWdacwqGJ4hkXkIa7s1V+2/68jkGN6XtZjJWjbvewrggpXChn5zPGo6kudtStY3RpvQ6hQNysbFWfwa1yHkeQXGmOxDNvTF5p7OPOKInc9Vmhe/490Er8iuv896KgUOtrzund1CGsjmFxrz5CN6DVplaqEzjUrPtqD0H1Yg/XSM7xMJGZ7vgeEAIBk9dEM21bAoxD5DOfUuPBOmSKTJdZ824uDHhJ5ScIBiTIjsUipetP5KjCUjrLzeS3t8J0anW9V7UP5wAZ2rEGQX/w8YlewErz0Sw6WnrQpryGbPFgLFPWBUwM5nZY+Gu0evrNWLunKtD6N7IMatb1YZuOyjDrEE3PqAHw6zgQANFqsd7F6voxiK4ku8YjIOFmMMfQyyNxO20DwtyQgLLbnOpqOAIpynEXNPQPn4eR0eotyGeiPrnYnzXc9LlSlMsJWKFCOR5DDrsxbcrAE+S4kyxf6CrbSSGNux6XzoSOFnH3qwXYbgBF4jwCbFvagndCutfTei9Q0oScdLZIkLYyee22so7TqaWFZ++uNz6NDltAJP7JxRtJly1pL8G+jP9g8SYPYP2gVwC/Ff3m0TqUK3ZdGDb7b6lTbJ+YR43KuwCTuFglomqxW9Vad5QFFmI7zQLB1Pvi18HbWoO8oxcXNC+69Hl4IPX7raEax2d9zviA2eeCjjRtjGvhmXOJgdY4WCrs/wYKuJAzBWxVXv8333dSEkbWzI1KSkdJN0zq3NHv4AQ/rPcj3ubjjr1bm8I0wr41aZIVxP9LLmbXCDi/+UJkRhtXRBZbZJgqGb0p9hMh4jzhl5zTy8how4D5lCDZM2bzgYcsKCrCCu+TufG1xZ3SisWiJeW9TsDXG4X8QGU4gZ0iI1TZGxHsMUpVHQgeZsovPMFPUm6ZV+kkXe/Br1UNcWtFNZPq3U/r2zZn9nGhqZ+aqN8nhIMrDsC2tInQOg/0mgPUSzzyQS/bUle4eJXXDsoFdSP15FdMwtD2F5nU3o/5Ye5i7CuNbZSXtQh7HzxYBqcgzEOFDgxu5GHiQdnvKClGzlR4n1ch/qHcKEkVxk8xFVvHv0P7jTZtM83MHBUNAVjofZLJJetVd4SQsb+1y0BcQwlFI4rn/AELQhkBZTaVc5evPM8cW2ia60YpfoAXh7uRSZqKQ1frtGljWQTqGZ8AJsNaIfoJPq+02BYXWC2ilbf7Y24G1fOXHY8lJgDNYk1QdTZYM1wtgq9NoCSMxxoajQ7SSniAcOdkH7uAAE2ZACCjELwZvJiQGoKL86RwfTV9U+/YDPbw+9qLKZK33znqx4NpYUDuS4fC3u1NE8VdT1OCJMjqG8ovNKOnvqlBpHxM7g4NFo0x55RWLCOpNQrZshgtwkiqGUAs2YO3fp6MxpcIgcn9E1wYtnx7+09hfxSG6lRpHtXoD8CfxtwQw2Xqq5iBGP8UTyH/6m9apfpHjuM+VaEOGe+GWjuBYXsCOfklXg2THp7vTbehKNfpscd0QjJLZbzMBpZnUVS9Qg4+Dw4/INggg1jOci/PpruqshJSURlVcXN2l5YxJ3LXmPC8c468tWbaMIW1dFBLWXU1cU/ZlXL0Ke4MK3WjwoM0Eya1zfA2cUNSHFAkMzmh5oY3l0Iwn49mvOPPdcsOmPqCm5PgofACIYMf0La7kcBKCoxk7V0mBSYKwa/TLSHJsX8YRTp0XVCOoGbeis77aXRlbNr9OYxloDhDD7eQOq8R9OWU5n/25oYwqGY7PXYfsxpb3427IBq65PU3CFWcnepJaHlkt+7uOyT/Nx4JapsUYVv30e+SGs6HQlUww/4u0lNgOz3OX2qFpSD0swFjcDcyT7IvlSk/vlpkMIqUo6CQ9eMgWK82hqQtxW4A6moNb+Ioy6GEUF4CRQmOqq2DQOYWiZ/jvz/Xt6y10fXEaZUf1r7v4ar/Kv58GohMcbGMqlQixRqsg/17ouCCkF/QFWN4LXeOO/IvRUprL21EyLLg44npGTek3LSUZ599IhlqtOgTy9AFBesU7ABzEvqcHkP2z8YNJphF+WXxVhY9FbJx5LmQKnWhPNRJ2mxXmGLL3f6NXn3micUDBlRtPKqBQ7Lcass+Oo8dAw0RYoAnIGg4dzYt8pLROSTXrdy7JNMW018oPKkbiYMy7LD+mnyR0o+3G4hfkSaaifXkh7tgnN7NZ8k0S8RIhHhC+bNrWJzXyo4cDhn/iu602Ud2go9Q4rrgT04C8Jhe7+Gcn/haeFVGLbZQq8U9LvbxrhlgVNnrMDi5p6LPZMm7QBDR7IW8xisuMu7DEksRRyZCF5phkb4I0sPfPpOYlkojgYu4CjHSw0BvJVkAmKOXI/UETfFtNNJV9pRzDf/DQLdXf2WI3MITsgnFqnzrZMwVYUbKl9PIOUbnpMBzH7qxZKaEsYS0wEB+c6lCrWmkEXXk7W7cj9CCxVHaGColbx5eNkpiEPdQj5ngmRRwDmMgSpJkGWYff1dT7bIG/b7Ibul4dbXCo/TRnWx9aZhEhqyV4omARp1JThMjuyuFwUMG7yZ8ULcuam5Pe/wOwoUxTQ6If7xtiAcjduOEXtuo6q2CBs6/9hk7wUB998OVReJHpdfOxg7k7lRt/QHPFjhchRFjOpzocFYjGcC1ZIymaXAsusumyHYbO6JLcUqyK99gpNjG5KTJsiNL05H/9d9LOz6f9+Kps4wGCFqS7RC4LJvCP1wOXGLvpr0sdl66Fc8MxSqYvagcdFFvLVUoE/MTcFgicAtIRzydjUfE9MHma5KJagAHAXl1rlBM9s51shI2kQaThACI52DB403RmTv9KQwql01eV9CJKXKGkqWPIKvFPJidGXhD8Nfv20Husmc0B6cshgiJ0OcDuTNvGCctBmkRZ/d+D5Bc9tDG4VAK9wBA2mpda8grurKr+kdn48mV2vX9LrTo8gpNoYGY/cn8z8vNNs8ymxj4q/MhV3F8Uh8Y5/sL60UOAY4eGw6FaysvA8c4A59mik6NymJ1AXMu8h377CY9wUPbXAWwperOo2asuRdBfVNCFKQro6axzZA8VjijsHZFmfa201vLSXbjLIyvPQgDUL4C5buWIbvkUSBNFVUKlscBA2qIy5dgCX/NzoR/ncLJ4du5dHFKCLn0H8RUsPvOD1E1BgCtm7pDHaduDtySLSxltgfN29UrE5M1X0qW8xRvSojGlwKR/ZU7RX0uCAlQHQASAOZz8SGzvmjYleTiimgX30Vyyyl1amjHXVgjTrqvsDuChw4he8J/yDNozqK1SJElBf4xnfxl+zoitxrzLbW17CxOnon+M4WVvemrbw0Hygv9ft4M0C36h62iLIlXzu+h/uaqdtbz0biqhMNlc+yOldN1HRRG54lt33ZGlMwRXptT9ed7vv3NrxZGbU2OVXLDf9CwzUxVaQwoYaMHM7WrOMD1I2atNRtEw+XHLXvsgDZ7xY807pEbKHHqP9xjFy32TrEV2cMP7wVQrKK7bW7loStYghYvSvKGhx+76Q3Mv1QEs77q+rv/BVBrLP7YRH4y35/ecakyiIydwO7YIdBB3Jyx7zEqmyP+AecZjHSmlDz76z7/sG9AqCf3oA8nS7H/O8ey3Pgnq9zT5qpvwAzQbCRgswhTDnTfykss9mVzm1eOEK7ZpIUGqnzf3HZS6D4Sj4TvGBolmhIYoMrKGh2nimiIf/1HwolZV+2Wwh1e90fCnuJKnNkurPw+29UXNtmicqfCir+/2RXW++7Giwaa3yFHwoA/ZvHA/087ORREm8WDosBoR+Q+0f0EvsblNfGKJ4d45+YWjlRt3RHCVHg1x+4CSwrEiSKf6n0wkVb9OQMnzymIKzaIyCB1a0RnBF4tCB13SY1zcIGt6jxQU65ZPhZYHxXPCO6oKGDAtgiu/sTGiYLprElfCUVIeVS8wfu0CPjB0B95GcNEMsfh/j+Iu6e1lY8qeT4NJQ5tAxX6FBBG+fXUhB1megQE+luQGHIgH6lTz6Xn+NSC7yaN8ffFPiK4Tl8UOE+pKOnx7hU3/cT6B2N5iAUGzhf8N3n0zKPlX1WOgEN8ZW4PBgSAuV3hKxjQcunoj5FZLHK+6GNIL2vSz+Jz6w09Vg54gz/tPnowWHt7EB6NIJDOYCqlWDz7b4Y/+eCRL8zvvwZccgt07bmFsISL6nAS/u97rQMYieE7N4E139GUsy+oYaTzQSWYFV6byao9nu1thDo91kF2/w+6F9wZ2r61+K+1gww83+luuT7vbWb9MgFTydxbvZg792tQFif0NPPTEYbv4aSUeYNFvJqheX++8JBQ1UqmxTw5LWpgm3icCDl8lIyGqjKeCgfl0BJ3SYrDKudvDfCK+CKLuMYPixkRz907huxcDeuDi7RP/HcY0lxA7HafE4LVDSOiIHgUcVwYAhMl8dFvKdz0InlXOtgaRGdX1+aoiRrHmTIGlS860iTPuob0VVTL8SIWvrQW963X7bU+ajwQkPnO7v/XHcsJg0L8hPt1ZGVpdcs8V85p6RuipLqsKeA7nu3gCau7S3SW8pZL8FnzUJLNbfij9ud1hMPQmqatYgO4nk27605ddn1zxB8XzvQTbWLY4vA49P3BoQQAxCx80QK2gzhWDpWAp9IjMlkYUeUjIA/xR0RKzFi6suJ7D6v0dTkltO7Um4BedfkdeXLO/BhPyrw0pBcLBbJVtXvEq0SfxL6XrSTqGQrz5NhnS3TSdBdZs15mQ+B15e3tXPuTNAzzHrA+GmDlf0vrKrPCxwu--bjV860iSph86biSM--eO2U1UjfoPVUyTzhWcIu9A== \ No newline at end of file +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--mE6HeB6Fsvd9DHxG--T8mDZYxmmz5GxmQ8ikSFTg== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index a95dc679b..9dfe3025f 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -mBTUPkHZ8zJzdkRMFLpog6AtZxgF8GhjMkfwglHu0p4DenPYS6RhiCqoqPVrsJ4wbeSPgjA06tSip1UzaM43v0HrRZfYsksphhuwU5GF/Y2uFErket4kTKYZMnO0c6xS/to8zBR9JiaiUP5M2N0RAF7TrClA+TftMqLOzCJWeEHlFTsEEt1IvQtOstVlhUXf00Vytzyo0bNlxltssXWmXpGPIEFnqGWst+c8o0anZLyN1XKOYWCiA2HcFAKQZLjW/wEiIDWqXql2GpLcLHYNXTUizfKa+Yf+2cwLzFZUPpyUles0zx42hwoR3ptZOx58ORzUNAyK21H38ABNDpymsAzk/28LyFFt28dQ2WcfAAUf50TbCGJuHB997kN7+6Go7WXAYcVgvTxkuRAeIwGs+9MEIA32Xy8PQ6h8dQ6fvL11Nid4wAFVcAQT8xKPRaDAy0IyZtpVdeik5hmD0TCMgwhBwXndXAhK5bKlzJKeA98cqasKy0HOwQUMCjD1ATW0TmDj9dm/anHp88nHV5SZiJRSQV/gLtkaevPHVHMX72G03UhGOMXest8mt/72UrmnQwVW6AXveK6N6X9YfDRDeCupr3cGagAyvAxdwfa0qOj5hMCViA5H3YosVxJGjuYq9FM5U34O8Xq9WBKOdLRUNgTTt0aatCiME0dd4BMYaXHDKg2RkTcikHMHUGczUuKVSSptN6PjiBsT+T4IqAUP4TNQCbfJNSmUnbsZz9F279u1hVpk1Fu666pTobu9Rs0DZRy7IsitiWvL5ghLRo+2DDGcw01hcWsrN0RUYed8qEOcuQZNbzyw/IWDNUfu0pchLP5zg6FoX+4zXbH4PdkyLmKG02r7x+v3Sh1uAKEkpkBtrnRqlMqzTgHmFyrPVFgug8Sjk4uenH6KUyXa+KVkLFUIFMGLdqE0d0sLpRG5sTxoTdxHlYrubF4SgfvcbBc8vNXVydbnGELe4tm/gJC4lFWabEGTZh8p/AflgqXewnPUL9bujYQ82O2uHLqZGk3ukEPW+YJAYJ1cjs0DumoX3fjiFqnVgJC/W0u44R+kx1dvoF/w90NJ0NbNvJlOECGsQj+gUQhQ2Dbz0V9VlG6OvPYxW8/dx0uJThv/j9BSCA1p0RmSsDPP3QJnAxJK24DD1YSQBn7ejnR11o1vo36XzLemPtPN0tIhjopnIUb2yYKi7ZMLaR8wFLeQCtt3Yj1KCir6d8SbFJeHCY1xf5ZAohzSDX44jBn5OIPTsPtlvcZ4SHg41z/kz08u+57l5IQh4mplqEDMG4aOzTZw1x0rtY3xv5/gmM3mK12O/0+pwxqPIASyynpwd0da81Hjm0ffJEbNk6GcbUaXEHX9ri6WRU8tRyL+d9Csa86SwV2OUAHqkVNIgOIQ/3/Jhz/bemrgVVT5T2eBq3F3oqbb8gVG10jAy1ehrmrPPniuVPc4Ju2eFamZz19ro/RrBz9fD49pvEHXK/17RxLKMsG0YvOxuFc/e+Npp5Ee7upU0WcQmgjIwARYkbcC+756lNpmMorcvMi3hKrd5gzACHA5wXQ1OBEKofR4P+0m1HTgDXgYY++7nfSRZj9/sN8i9tlnDkUVfA5k3M+ePGNtIQ0DfNR8p7T+xAOGzzGwtFoRAf4O9P7XkpR9uL02de49shVAv/twyLw5zHlimVa12MIu0FhPnGYVjlSfLs0dReB2G5tQKV/yepxRZNdJlylLT17dW+2HktzziDM3Y2rfLNfeN4FgxBBdMaPJFmxizWjNdQsKJq/d1h9tVQr4u4ltfShHdnAVWYKMONEG3itedDdY3U+aU/D0rZnaeDKs8ZjDfyMA9ZJseejbM9vYmYmwgTZNd1Il4vzBjAWffLtGATn/Ib74ceV9nUIz5NB5/UKKhbuCRbv76R+uz2zakOlIWH37DqIxUUDkGsxBevEvQKMciwN+KwF1ZTGA9yuatEoGt2xI0JbncPE41QzmrO7DaOu5ayk1nyBhz91ZVfFLEBlQSkJjCRDEnhAkOVkulnPI6FxR7wEp9xPK66v1ny0jn0gfeTzMaVdnv6cGZmYq0cUdsOgqSiZc5rGXsK6O1wEZsRzI39XTMS2Z99NyojC2SIOpbnsxdqVg/GfFfL3bhI6zmy6+MnOrkyoa1guzqCCshtQVgGNNgLIEwwNyJZwymgpdXjHm7kbdzOAu82x9XWhjNs3er3cDFnj0sNno+6+bPzlvcBmjqG9IoerJGfVHwOkT79eYYG9UZi1sYIL6rcssgK+1dnIKGmgZC7wOlqmZ3uQRuezg9DArF9sGw4vrq+y+7y3042SoCIgC/W9JWnkSN6NjljWlRDH9Awg0ldl2hMJNmfv6wgNhqn+LNfDHMhUsQhv8k93ENJqj9tnvpFixOoo0oTG86jX/dDLAhH8eqNrB0gbj/rJHq7Usy59F7LvnF95tYf+JAMP7YpB7/5Gldd0h1SieuIW47YmpXRztIQ3hTjXiLRAiRPGMJshDmr0+lrjzZJElpryfs//qM8qATQMlTjdlhSKA8BWIiAFxLG0xtuhnwjBG1+h4mnSq8+q4LXHBeBp0xshK0cF9pmW6aGmX4xk8QTyiIuDF4jAOQSTLv08Nk5Eci2W1yQ1Fkgjk0OIlgw771c+15Xjgn2L1AxFd9iOb9niVaYNPsM258Gjub+nLS9aQNVB6fJmolJTnSF9Qxog/+jDL6mgv8KrmLgVFo+mw8JHlh6f2UWBYiAjeqxEqJV+1sy3Bmk0IgaYzB4DL4Q3HIcBaTdj6AH0TnvA3QWs1D/53mxEd2XrDxDLrwclKmcPELEY3CrrvLZkbZmAmMcR/A+wWwNyyTsK0VNFw08pZoqTk3Ppx32lLEYW0y8S7v6bSjdjJx23a+JzJr3vznGBEUIfE53FmAXDZJie+wHbQvvn6KL7d00dSzeyhO2W1GmY3d3MTiTYqIeGjMlsfTPhHWsQBBF0EXF9f03ThA18jzAyue7I/ld5E4/iTEyaUsYtfr0QMF9RoGADtdziMsPGoJ1Wgd6jnv8p/CUiNff4WtqYsUUt3wJ+bZjoFdQC3En+CFctrXOXh1G7QkKP0nVRPqYqyhm2SeI1c5Cu8obU2vfJcfZ3swYhIZxu8N1tKpbP8XddX6i03wqdpFuLsl+2lz1iaHl3FpfJmKES3pQcGatRrW9cBK1pr6d65Ii6c2lly6JPYQ7AatlzVrIfhO4uT2tdg/saIqPoOAQmowz5/2Quxh51tszPiBr5xKB+A037EO0BiabhsMS7wIzF9NN8griQ+OuKFF1ukQP8NMwt8GJP3YJyYzGlxRzH2JWzMHY4ObS6UwdIdfW5oatLivz8/Es3huWyYguVmdyF/TfdWMQv4LMWjM3iir6GkJMwZ3wAn2il80fKpzJMv7dNClDMiLxVlLGskv3UiwzVfmRlRgi+yBL0LQmqyxckIbZz9lBfhAO6Ul97uqVhynulwCqniwR4NUZzTRxrGJfDOsgjGIVKDPyMQla718FKJg9uhdhrXfIlmbX83Uj4kVlUpiY4+aNMAegfpBFRebYCH9bCPpJLBz/yzIkfbXI+TV7RFz9yjLJenA7jQ4e4dr59r3BK7D2hFAzrFies56VEhDMkWdrg9AD2lfydLlcjDFmIRuVpHykZ/RM51sduVr38KF3dTFv0x5/9XT+RIAMVeuDl3w7tR0Phd+J5W7lEkPAJAd9G6naYS3xmcyZZV5yrajFc+TX0qcnldVKpyPMPoY/enCY1U7Cw2Dn6YOxe5ejyDgVpEarNdapvMeDbLxmm9TxbPDmFyFwV/HpvXUCcTzQZnGGf0R0rhS+qvAjHmv+pbgEjGrddym9bNG2tat4dAiea23qmUR+AL3FV23m533AUOQZnrHmtWwJ9JN122KH0FHBZS1Iy0azo6+VoJaQ+KwQGVtuQ3czFFZPW/zUCqaAYSdn7dxT0yG4qqtpyUTbfX2CKQ7fcDm5LZIIrr45vt81eqMtFeQM2mJBCbYitAfNz17kjNs9NQ2g6z5G3qnWyGBMZ73naI9l+O8YUtlMUZeHJOnBwACgQwcXXF9AKo/UfbHKgUm2PnIMiRjYmTHaFcfXUTWBW3GnQsoXxZBW1RXjbLuSOVbH3UilVWCX4MKtAEIYJrBplSWOwHOKERep2356dI5GVd3B4mMdKtI9UMSd2Wgv8lF9L82x5BOs1zq9aZFuPgyPfebP+V+cj+a55weZJRoptaq6I7Lyk/K1Ul92itqth0+pLhJU0ACcKLR0jAIB/6qCZi1ddtJXEmc2XxYAnlpiqEMhTWlZc6DrTLYIBHeDiG1yf0mDQO7wlGl/gTfZQZGQhl1IgtJVnazgi/Y9s/2/ZO4Q4pgFCGEVW+HTDfd/R5qYGTqiy9h3DKa93kDa0Rk1DoKZ8ZxdgA9GhGjMqU810CWkG+jRa5b9rS8cdgk3XBxqT20GJDiKtxGUyHQ148EpeNkXUCNX1grcY/T1dcmvIi0HPS2xR8Dp5hpWazsmksQvE2Lg2vzi7XaOSwG2//sLuwGPxCBoB1l6k+aa+KvwtiToXISrY+BPXB/ZKFT6IYPIZE7IanKJ10bpWzCOiMn8T3HZB2nQ2OLhadZj7x7pMHwXxw+E0rYRA+dlHVNRX8DB4Lbe4EAqlscZE64GeV6W0wzs0eNawAerSBCK+xckBXB/u7mPT5Mkumc+jookVEDymMdEsP67/UtudPVuo5hRhLx9m+y6sMK8RR8rHuHyQFMz4NfWOJLdn4ySwrtAnHwjSsre4AzAKjUDkRWbLLttIDROHOZJLrwJY2akKA3MEQQQfvm+HTRbS0K6QJDa/zeUjhbSVQ+qqRoqDpXhSFfTWdffVWK/T3Jo1FHn+f0ER47CUfI0EBkFRM5ugDJNlqXgFT461FjvyudBBVICtaK6+4rY/QqNtaBHY4JAWa6CbzfbKEj4fN9ptExUVJtAM7K8igkDJIEqM+2i1hvqzMzeW/JcSWpxAXhy9qT6ZBQLmjrO6gu97rrOlS9R38pn5pQJVatCKHex+tUmnkBQNa8kKvSNS9wtEKZzDt6ch18dUytPISh0r3yp8Vqfji28nqNRt+fe6ok9iDYLcXw+azRjbqwcl8W/RDDEka0uy8gD1EXSxEjcJr0CwSm6SRtxJdFJ3Nr53i0TfpUWfLCGbYjOTNCdryVyEtwEndAespKz+wS9LF5rj7PztSsQeU2+5Wau69T/183mQohKjcKYO0lrUvDSIPYjr+NA6w5Eq8sUKMi08ZTLv4W2MGSeggsC1nWzoJ1MiMIH2B9PAyp5bm7+Opbt3Y/3V1pWv/y7xDTuktOPkhxw25y+PgLEF1ia7jk7FCjWoq3DWjGcD7MEkNPBg05nNH+Uqmp3Tlo8brNAvHQuVOhhQpiy4POL/15mkqsFfQ7Vh6InxX9zEWADAHzo6j2qYsbLdnNsy6s0EnPJ1pJOiiKNHt+oJjZ2qjVPaEyHxPSqU8jV+3J3qm6dC/RjdkOfrusUC+P71pHrfP69msyHvyYLBlNqfXJyYhf79HQHn/l3FuVZKJmihFGOuDVPj7me3xJ4SPPIKsY4v2YGbcLeshRRpb7XCVH2shARKJIepq6R5ZTRvGFWKPpoIIjPm8oTUxDJgJXYOCH5uUlTcDJvKP6E79z+HHBbzKTSfc/a7PHs9cFfsKyAgmZTElBJmJdYY20y0YplNVXlx63DR//Skn/vFjDqc34zhUWLS9Pdl8ckNZ22nzsCkG/UGmgqZ8F+3E1CZ2ifqpcW2oq2Kh6FjzfOdzYo65thZw/bXP9qPl4ZhyGgPvL5/VRVkb31SSVpGcYdsdRZxBwPVU2rE/P4qLOPFegIbQuGMI9+702naO5xFdI8Ax7RVRFT3N/Z5+aB/YmJMd30ZVfUm9ctlWpCNif9tBNxIC/32+v7d2sLNM0JlqajP2Ivpw3LU0KH+Xq0mMgaZwYsoiv/K2KLyeYSq5zgCS+g3dWuA2Z0L/UZPit4gKlCWabAXySfboBGgn8ACbpaDiEfLpEVAAZX/baoWLkBSzAc+5jtszdRG61boWtdOlA8ipbubcm03/mXgW50/JCzL4+3TOnf+2huIEo3ktyeRixF6Z7UoCPdmY4UA2KZy1M2voGrN6GuD30gNRpHznQxltCh2P6ZC6nacmuALlHygiUUcKeQ9/+02OkGZtmkDxmj9I7JsQ+d/uYneu7vBO/BX4VLMjSiJBBb4UY9iWMq6rbb0k4TXVX/rcSDIPlXwi9SKG+enOCy2LKl43fhW+DReLpkAQx/BwUwhEFr97Tps3pGMDEbzoyWwhW6/2X+/TwxRxGDJacX8A3Frw3EzJ8cz4Tw4uuAypm/kerAFv4oxQwMShOpIQ8YUe47zWcxVOF89EPwYayYhf0jntkWRYs8HODQFjtTHEhFdYps5CEWZ5ql3CzqDPUdk6RFfNCTo4SVJ0deNOUC6RHCZX4NU9W2HufHx7ihGDDIa7DQev9eTRmHNZDRCPrXYcLxMA0XtUH2mypRXcmoayQ1YCoEcYsdFeesllm11CZhvJK+ZbhSoTSOO2AMK/AofayZv+qAeqw/8HF/yuS0bm2HEw+cr5DAol0xS55Fy6EmVzI0lHRman9mdoKBckO++iVp5WkjxxKcW4G5TzazRdVYMAOg+nkVjpuxdEktgZmEaXGo3X33b+X98593vVbcQGzZxqygceHQghmk+2rlFiKe9OVjnRkH7TUjTCO/Ymu7JH8f9mV2TPIYQgMsHNK1AGAfAjZ/Dn6LsF2ULGh4ehhjoNykvRYIMB6xOZhkD6cANFm81UBRn5XeCFd8MU4Elm9TNTd1pGwjZvZ1arYvdGHLl0Gg0tJWnhSMG0qcfGPN26HBLgF+/pUdIF1zm2Z0TBO4TuaIyLIG6XvUOK2FAf26+l8JMPUOROauzMVx92AVzDO6n38VO8kmP2480BK85M33YgfI8xAfmfAnUb593A7j1u9JFKy7lWK0qAhAZhRA9whhhlYFhAkAL2N/HqKHxrOj9oLDItwXIulEQTyefGbKuIzd50yFVze5jicy+Wy8a9EZ5FWhkbRvaWbAbMeht96Ilc8zyLfjCi+kk2i7uCQCGdXMFXEBroDaWV/1R0LTYmJOq7n2YeCHJLq+Z/PtXHchVDgLsrgLSnrN65owxddDCx0dvPDrCJJHvqVNBOr6S9xuzzvw7kmo/Dai+rIF7YxmWku+utsmB1ykuRrEUQWPigFSLunrhhDzXTeUuEtxbkWeYeM2WK4qwuUaYCZGarrYdbzDtj4vDnv5pOc73xwcTcgdJtBIB/jb3BGSMQQ3pCtskxgLm9X8PCCT4FmDkloMqLjaQrB0dxT4OP9UcOlMewo1QyrWl2592XochNhH4BM7TRjYNXy1/GqCjZOevZ3AxKdy18fW8WwIfw8S9fJqdgtHTtmYqlYMfVmY/+D3gVefNHK/3m3k/PeoJ7YbARgNKJFRqmivxctm4+Exm0SUWAmfUP9QudgyBx2pmNzIMdcmNXHyYfFIzqetePXuGxYfsho2jMyT3VBoJFbJk3xzKYzryQ4jcc8wAkWX7BRlpeMnBbwVH5ZNG+IDVqeteTU5mErf4XbV20HNoHVPXnM33l+CEMpMKy4jfJz7EDaxgYnmY7Uq0k38XePCR0gECQZ8Vq0B/PY19yJn29Q1gzWJwcAV8uNFJfxeFEHk1lOH8fwbT1Pj1itxV8H3O2dLekqIrCXaPZhFUXpS3s0xS7zI3+ZXtRkpRzUdjWrjYPBey3UVmm6SpAzMZomM5V7LEbNMvZMRRyVLsaM/OJjRKdv4l/OEWV0E17p/lHQTiJ9Gq9iOc61QcJveLnVpqf1ZqC8grEX7eK0pMDodtw3K+7xOCThb/VMthsct4l0O7/Fa3j9+J2IT7ZfXdFjNcUUG55qnDOboF8fGDN53Mv1s/dXdwpOO69c9RbOA6oNLJQKvocd6Nnzb82sAc8HTdxWI2cbFlVbeD0lkEWUrncTuEAPF/qkzED0EOrAwDlKO0EAGROzGU4jgA0QQRRbGzZyHScd70YnqN/6lXakGCYhvX7O8FTV9nVQFFPPZs2a4d3VbS5ijPS9bCKEkscdHw8X5UjBOfFdfaCmKpRTzn9mWWVeUj5CkzY+NDZAJ0eklJACz6B0sGtlTkHrB24qpvv+GJ9HI2TuOsfREhEn+EourbmaD9fwIoEFlha5RsKABUD/Jby1Ooxt8nJ/mUrH3EOyGr73l0RJnr4pXNX+sguwSJHqv6Gh8PUx4vk0QpMXmxmHMUStXDD+DVvzpBuGdB5OwOICj3aLUMA86sFQpnLGbBObg+HNdFDGJ+374ib5ks5r+FbYLEwbMkQV2eW6KLcLzcSHcnVUiTz1cNDESJWzd8oh/p1olb8gjgadJG5V6zman6KtenLV8JRVSgSX1kqkrbn6IK8H5foXWjmBA0Ec7m8e6bPpQ6d2EbBNPxiivthdKbsD/uqGndF6ltL45t5VNfE4gkrUa07a2S+g+yBaP5yc0w7Ox6x8U2KsjV6rxmfrJBc579VsfLYUMZ9MqjR7QqKoe9yLqpPaqiPBJ4yRWOt9hzRxynlu12ddPd8Ti/3CQUHCLCbPO4NQMr8Xa6azPV2CqHFC121rYTPPBJHKdV+ohdx1nmhbaS152olcz2D2cP6iaZaPSz4mS4EDN5CNHu9rzGeCiQ40PC8IVkY/FZdOgO0tR5L/eb4PDxbr+XsImBVZhI/BIu81fvh+Oz1R1qcrkRrfVIF6pnFjxqeLd0mhwa2vO3qfklNTLGV8lpYZlFNfil3zCpHnqtUcUlGVASfiwmIt4ouJbbqlEezA+/O0OddVzhJ2KhIwPQVclLEWqJAwdm9wZmZ0F5gYZonG5nl/Gv6YgwVEScQjSEMado2jAvS/i+Y9p/4jBlTkefg99F36SSDMEZXy+ARB8yU9uF3LP7k3U0MOPyWW2GixFgobSQl7sYrd6KD/W+1G/Z2Ax4hF7wrRUoM5fm8Lnj8leEnqMU83NgB5AfUWn6IlOeyF9S6PjmEYVnFfgOU8e7xoLJrelT+a+iA7+zDzAIbiIWd3nFD6/vHnujUvI4u9WexQw1Z3eldh/2ncYeveRFAVVvqFZQAGW4k/gSqnhUjXxkS/AS6cUv4Jd6xt/M1govuJkUxBrwS6gB1UmfgtBidfgH/ABtO4Rw5dWjp6g4Tf69Q1EjTBP2JLDJQiT4mDI1F7WeiFPbhh1e0u9oMBo+UpilM5HiOyR7Xt75Gqo/G+3v61OjLjQP0rdy4MdEengJu1NpZLVxSWjK5gzIrYfspor8m732hZA+7I5hkBzJFEE3nLcbgArf3ySvnY6BPWotqKol4t78v5VJW/brDzd+smQurFSy7pRFAsBnwiFq7Ry+QqHX17orQbsOZqCGXZ5+YnT7ec2iebcL9yRh0mG8HDrisIT3FWvuH1zR6DBq9gl60Tj4RUYsCEwnOC/ggAwMQ9OHuepjMMS5F1BEHC+7P2lpMVry1Bp0ryK4BBVmbFvSDsM91OOBUD/X6dOd3eApxRDNNFDNUGXWP737UipBE/+V2a+eg03BhAVy4TXWjeOo29x3Z8CdYGnUwFcytt7jlaMAicEH5+DL4Q+J6BNrI11ypVbbHhgZkbwSM7+bL3zWmFmW7SKZi7liwlpcQtm1ntYvLD+WcxKbJOlrw0uy8dfQ5FjHNdtJoSPp+FLWEop+LSinB8SdRiqHHMck+K37wsB1N1sbKmYc0JAov8l9H6W5HGP+SQOFWuMlXpgpnX41MejkOU6HZwvgBL5NSgL9cMYfwKjLmHRLKbiCAMmMLGGWXQ68mBp/BJou7WH4q0dTH3Y9i+oqjmYbuSlVdGzJGOFyzPpf3o5oKjD+OSD8x1dRtkqntEicIxXFnR9dHpkuoRr3pM7lpgUng7eHiILRhTFO96xzc8YrttnvS1CTAG560f6w107MRdnmYviaRXEosbiDrrV9Khcicr/oXNhFsgwLtqBSJxVQAvjPg5p4bWM0F3skLUgSDQjiArdW/jEyt6oLWTVRM8cOW/YJMAzDt1f73QYy/qp6eiZzhhgzIYI5kNem/aJVHrmbpGMVA7ZB5vrWm1PSKnVBDNTuGFMUKuGi5WryNslY5ND7jcpY9BMnCYANlerxegU6NTaFOcwehIBwGwx6LvWLe8ilhaL/auECYTS0ig3h9CgULR2gqqyAt+c122HkAOY2nCOzZIv7B5bubbCtnSK3ZHuiLD+qvoek2ecyp5RGoml7MbDR3A4ZwYc8Juh7MsE7TILzFks7OGOWbr5z44r+sJibpWl8UrTw9HvnoJofMgrVxTc+a3aGR7tj8zgfPmqQwHaDdRqGkR57pOR9TiIG8h2DVfNR5TAQrgtiKE92GHxb/1DVJHe3CXq8dcqYzU0H+1EhUIHMJzN7K+m709V8ADwNrYwXBDYNU44j/BMWJ0JxnhRWiwyH/m1hUyFQiNlwiXNL8WrOj/6mKNsQn+AbHiGPq5wRd2vSMvHd3ck0838K9lVxmE3o8By0InXPCpjPEXpqeu+CvEonpMq/n--o03/wyKMqS37wmEr--PGWkhEX04SURN12/rZeEDQ== \ No newline at end of file +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--D/uaMzAQH8aFuO+u--HkOCuZK3KJsvkGlaRQ45Gg== \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index 9f2b7b14a..e7e3894a0 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -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--F7LPrF7SEIYJasNO--J/H0MGGLiOWbI22U2DGy8g== \ No newline at end of file +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--Jz3stjju1xhM5+A7--5uwznqznRm9MCJq4I4/q+w== \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c50ca7eb3..050e97991 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -314,7 +314,7 @@ def i18n_message(default = nil) port: 443, scheme: 'https', host: 'oidc.integration.account.gov.uk', - identifier: ENV['GOV_ONE_CLIENT_ID'], + identifier: Rails.application.config.gov_one_client_id, redirect_uri: 'users/auth/openid_connect/callback', }, authorize_params: { diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index db2abbff8..3d9ce3c56 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -9,9 +9,7 @@ before do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('mock_redirect_uri') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ASSERTION_TYPE').and_return('mock_assertion_type') allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') - allow(ENV).to receive(:[]).with('GOV_ONE_CLIENT_ID').and_return('mock_client_id') allow(mock_http).to receive(:request).and_return(mock_response) allow(Net::HTTP).to receive(:new).and_return(mock_http) end From d4c6be6e188542cca9c80094ffb3ffdb7548d748 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 2 Nov 2023 09:55:36 +0000 Subject: [PATCH 021/122] update application config --- app/helpers/gov_one_helper.rb | 4 ++-- config/application.rb | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 58530c977..a0ec1e447 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -12,7 +12,7 @@ def login_uri state: state, } - uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/authorize") + uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/authorize") uri.query = URI.encode_www_form(params) "#{uri.to_s}&redirect_uri=#{ENV['GOV_ONE_REDIRECT_URI']}" end @@ -24,7 +24,7 @@ def logout_uri state: SecureRandom.uuid, } - uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/logout") + uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/logout") uri.query = URI.encode_www_form(params) "#{uri.to_s}&post_logout_redirect_uri=#{ENV['GOV_ONE_LOGOUT_REDIRECT_URI']}" end diff --git a/config/application.rb b/config/application.rb index 90a2dc030..39171dea3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -68,7 +68,6 @@ class Application < Rails::Application config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) # Gov one - config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI') config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) From c6c8ecacc4e504d49e010ef8185e07749108805b Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 2 Nov 2023 09:57:41 +0000 Subject: [PATCH 022/122] rubocop --- app/helpers/gov_one_helper.rb | 4 ++-- app/services/gov_one_auth_service.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index a0ec1e447..4af428848 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -14,7 +14,7 @@ def login_uri uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/authorize") uri.query = URI.encode_www_form(params) - "#{uri.to_s}&redirect_uri=#{ENV['GOV_ONE_REDIRECT_URI']}" + "#{uri}&redirect_uri=#{ENV['GOV_ONE_REDIRECT_URI']}" end # @return [String] @@ -26,6 +26,6 @@ def logout_uri uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/logout") uri.query = URI.encode_www_form(params) - "#{uri.to_s}&post_logout_redirect_uri=#{ENV['GOV_ONE_LOGOUT_REDIRECT_URI']}" + "#{uri}&post_logout_redirect_uri=#{ENV['GOV_ONE_LOGOUT_REDIRECT_URI']}" end end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 46521d504..7dbcb215a 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -10,7 +10,7 @@ def tokens grant_type: 'authorization_code', code: @code, redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], - client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', client_assertion: jwt_assertion, } From a83da4e7325fda7d29d4f24c8abac57759bdedd0 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 6 Nov 2023 11:42:30 +0000 Subject: [PATCH 023/122] update env.example --- .env.example | 8 ++++++++ app/services/gov_one_auth_service.rb | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 8f65f9bd3..629b0d7bc 100644 --- a/.env.example +++ b/.env.example @@ -81,3 +81,11 @@ E2E=true # Deployment environment ENVIRONMENT= + +# Base URI for Gov One Login requests +GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk + +# Redirect URIs for Gov One Login redirects +# Replace localhost:3000 with the correct application root path +GOV_ONE_REDIRECT_URI=http://localhost:3000/users/auth/openid_connect/callback +GOV_ONE_LOGOUT_REDIRECT_URI=http://localhost:3000/users/sign_out \ No newline at end of file diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 7dbcb215a..7f8e39118 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -6,18 +6,10 @@ def initialize(code) # @return [Hash] def tokens - body = { - grant_type: 'authorization_code', - code: @code, - redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: jwt_assertion, - } - http = build_http(token_uri) token_request = Net::HTTP::Post.new(token_uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) - token_request.set_form_data(body) + token_request.set_form_data(token_body) token_response = http.request(token_request) JSON.parse(token_response.body) @@ -92,4 +84,15 @@ def jwt_assertion JWT.encode payload, rsa_private, 'RS256' end + + # @return [Hash] + def token_body + { + grant_type: 'authorization_code', + code: @code, + redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: jwt_assertion, + } + end end From 04a8a15ad84b73a305f701b16b330e5548ca0aad Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 6 Nov 2023 15:16:29 +0000 Subject: [PATCH 024/122] add gov one env variables to docker dev compose --- docker-compose.dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7287b9819..6f104f3b5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -21,6 +21,10 @@ services: - RAILS_ENV=development - DOMAIN=app:3000 - RAILS_SERVE_STATIC_FILES=true + # gov one login + - GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk + - GOV_ONE_REDIRECT_URI=http://localhost:3000/users/auth/openid_connect/callback + - GOV_ONE_LOGOUT_REDIRECT_URI=http://localhost:3000/users/sign_out volumes: - .:/srv tty: true From 773178de60d063b1e23b023cab9470aec05273c7 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 7 Nov 2023 15:33:52 +0000 Subject: [PATCH 025/122] add feature flag and revert changes to application_helper --- .env.example | 2 + .../users/omniauth_callbacks_controller.rb | 18 +++++---- app/helpers/application_helper.rb | 39 +++++++++++-------- app/services/gov_one_auth_service.rb | 10 +++-- config/application.rb | 5 +++ config/routes.rb | 2 +- spec/services/gov_one_auth_service_spec.rb | 2 +- 7 files changed, 48 insertions(+), 30 deletions(-) diff --git a/.env.example b/.env.example index 629b0d7bc..5f7182348 100644 --- a/.env.example +++ b/.env.example @@ -82,6 +82,8 @@ E2E=true # Deployment environment ENVIRONMENT= +GOV_ONE_LOGIN_ENABLED=true + # Base URI for Gov One Login requests GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 75df54064..8cf771764 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,10 +1,14 @@ +# Controller handling OmniAuth callbacks for user authentication. class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + + # This method is called by Devise after successful Gov One Login authentication + # @return [nil] def openid_connect - return error_redirect unless valid_params? + return error_redirect unless valid_params? || Rails.application.gov_one_login_enabled? session.delete(:gov_one_auth_state) - auth_service = GovOneAuthService.new(params['code']) + auth_service = GovOneAuthService.new(code: params['code']) tokens_response = auth_service.tokens return error_redirect unless valid_tokens?(tokens_response) @@ -39,7 +43,7 @@ def valid_user_info?(user_info_response) user_info_response.present? && user_info_response['email'].present? end - # @return [void] + # @return [nil] def error_redirect flash[:alert] = 'There was a problem signing in. Please try again.' redirect_to root_path @@ -47,12 +51,10 @@ def error_redirect # @return [User] def find_or_create_user(email, id) - existing_user = User.find_by(email: email) - if User.find_by(email: email) - existing_user.update!(gov_one_id: id) - elsif User.find_by(gov_one_id: id) - existing_user = User.find_by(gov_one_id: id) + existing_user = User.find_by(email: email) || User.find_by(gov_one_id: id) + if existing_user existing_user.update!(email: email) + existing_user.update!(gov_one_id: id) if existing_user.gov_one_id.nil? else return User.create_from_gov_one(email: email, gov_one_id: id) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e25200506..b8f45ea20 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,28 +1,35 @@ module ApplicationHelper - CONFIG_SUMMARY = [ - ['Rails version', Rails.version], - ['Ruby version', RUBY_VERSION], - ['GOV.UK Frontend', JSON.parse(File.read(Rails.root.join('package.json'))).dig('dependencies', 'govuk-frontend').tr('^', '')], - ].freeze - # @return [String] def navigation - render(HeaderComponent.new(classes: 'dfe-header noprint', container_classes: %w[dfe-header-f-header-flex], navigation_label: 'Primary navigation')) do |header| - header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) + govuk_header(classes: 'noprint') do |header| + header.with_navigation_item(text: 'Home', href: root_path) + header.with_custom_logo { custom_logo } if user_signed_in? - header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) - header.with_action_link(text: 'Sign out', href: destroy_user_session_path, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) - header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) - header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? - header.with_navigation_item(text: 'My account', href: user_path, classes: ['dfe-header__navigation-item dfe-header-f-mob']) - header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }, classes: ['dfe-header__navigation-item dfe-header-f-mob'], html_attributes: { id: 'sign-out-f-mob' }) + header.with_navigation_item(text: 'My modules', href: my_modules_path) + header.with_navigation_item(text: 'Learning log', href: user_notes_path) if current_user.course_started? + header.with_navigation_item(text: 'My account', href: user_path) + header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }) else - header.with_action_link(text: 'Sign in', href: new_user_session_path, options: { inverse: true }) - header.with_navigation_item(text: 'Sign in', href: new_user_session_path, classes: ['dfe-header__navigation-item dfe-header-f-mob']) + header.with_navigation_item(text: 'Sign in', href: new_user_session_path) end end end + # @return [String] + def custom_logo + [ + image_tag('crest.png', alt: 'Department for Education homepage', class: 'govuk-header__logotype-crown-fallback-image'), + content_tag(:span, 'Department for Education | ', class: 'govuk-header__logotype-text'), + content_tag(:span, service_name, class: 'govuk-header__product-name'), + ].join.html_safe + end + + CONFIG_SUMMARY = [ + ['Rails version', Rails.version], + ['Ruby version', RUBY_VERSION], + ['GOV.UK Frontend', JSON.parse(File.read(Rails.root.join('package.json'))).dig('dependencies', 'govuk-frontend').tr('^', '')], + ].freeze + # @return [String] def configuration_summary_list rows = diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 7f8e39118..bfda10fd8 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -1,8 +1,9 @@ +# Service for interacting with Gov One Login + class GovOneAuthService - # @param code [String] - def initialize(code) - @code = code - end + extend Dry::Initializer + + option :code, Types::String # @return [Hash] def tokens @@ -61,6 +62,7 @@ def build_http(uri) http end + # @return [Hash] def jwks discovery_url = "#{ENV['GOV_ONE_BASE_URI']}/.well-known/jwks.json" uri = URI.parse(discovery_url) diff --git a/config/application.rb b/config/application.rb index 39171dea3..215cec3ef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -109,6 +109,11 @@ def debug? Types::Params::Bool[ENV.fetch('DEBUG', false)] end + # @return [Boolean] + def gov_one_login_enabled? + Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN_ENABLED', false)] + end + # @return [ActiveSupport::TimeWithZone] def public_beta_launch_date Time.zone.local(2023, 2, 9, 15, 0, 0) diff --git a/config/routes.rb b/config/routes.rb index 9de3c4b5c..289c4f67a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview - get 'gov-one/info', to: 'gov_one#info' + get 'gov-one/info', to: 'gov_one#info' if Rails.application.gov_one_login_enabled? get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 3d9ce3c56..7cd6cb351 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -3,7 +3,7 @@ RSpec.describe GovOneAuthService do let(:code) { 'mock_code' } let(:mock_response) { instance_double('response') } - let(:auth_service) { described_class.new(code) } + let(:auth_service) { described_class.new(code: code) } let(:mock_http) { instance_double('Sentry::Net::HTTP') } before do From c12af1bf8dc3e137ca0511ecfeb0cedfcb1dace8 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 7 Nov 2023 16:49:14 +0000 Subject: [PATCH 026/122] fix error from merge --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index 76434eef5..f15e9ba5e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -113,11 +113,11 @@ def debug? # @return [Boolean] def gov_one_login_enabled? Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN_ENABLED', false)] + end # @return [Boolean] def maintenance? Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] - end # @return [ActiveSupport::TimeWithZone] From dcf4cf36d108a265d56a62946b5ac9e4e8745846 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 7 Nov 2023 17:13:18 +0000 Subject: [PATCH 027/122] rubocop --- app/controllers/users/omniauth_callbacks_controller.rb | 1 - config/application.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 8cf771764..f311ff375 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,6 +1,5 @@ # Controller handling OmniAuth callbacks for user authentication. class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController - # This method is called by Devise after successful Gov One Login authentication # @return [nil] def openid_connect diff --git a/config/application.rb b/config/application.rb index f15e9ba5e..08b3eb4b1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -115,7 +115,7 @@ def gov_one_login_enabled? Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN_ENABLED', false)] end - # @return [Boolean] + # @return [Boolean] def maintenance? Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] end From e4f9f4fa9ed230f76a3df761335d72a53dc897bd Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 8 Nov 2023 15:58:51 +0000 Subject: [PATCH 028/122] refactor gov one auth service --- .../users/omniauth_callbacks_controller.rb | 17 ++------ app/helpers/application_helper.rb | 39 ++++++++----------- app/helpers/gov_one_helper.rb | 14 +++++-- app/models/user.rb | 15 +++++-- app/services/gov_one_auth_service.rb | 12 +++--- config/application.rb | 2 +- config/routes.rb | 2 +- 7 files changed, 49 insertions(+), 52 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index f311ff375..6a1b2b04f 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -3,7 +3,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # This method is called by Devise after successful Gov One Login authentication # @return [nil] def openid_connect - return error_redirect unless valid_params? || Rails.application.gov_one_login_enabled? + return error_redirect unless valid_params? || Rails.application.gov_one_login? session.delete(:gov_one_auth_state) @@ -17,9 +17,10 @@ def openid_connect gov_one_id = auth_service.decode_id_token(id_token)[0]['sub'] user_info_response = auth_service.user_info(tokens_response['access_token']) + email = user_info_response['email'] return error_redirect unless valid_user_info?(user_info_response) - gov_user = find_or_create_user(user_info_response['email'], gov_one_id) + gov_user = User.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) sign_in_and_redirect gov_user if gov_user end @@ -48,18 +49,6 @@ def error_redirect redirect_to root_path end - # @return [User] - def find_or_create_user(email, id) - existing_user = User.find_by(email: email) || User.find_by(gov_one_id: id) - if existing_user - existing_user.update!(email: email) - existing_user.update!(gov_one_id: id) if existing_user.gov_one_id.nil? - else - return User.create_from_gov_one(email: email, gov_one_id: id) - end - existing_user - end - # @return [String] def after_sign_in_path_for(resource) if resource.registration_complete? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b8f45ea20..d7b80bbf6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,35 +1,28 @@ module ApplicationHelper + CONFIG_SUMMARY = [ + ['Rails version', Rails.version], + ['Ruby version', RUBY_VERSION], + ['GOV.UK Frontend', JSON.parse(File.read(Rails.root.join('package.json'))).dig('dependencies', 'govuk-frontend').tr('^', '')], + ].freeze + # @return [String] def navigation - govuk_header(classes: 'noprint') do |header| - header.with_navigation_item(text: 'Home', href: root_path) - header.with_custom_logo { custom_logo } + render(HeaderComponent.new(classes: 'dfe-header noprint', container_classes: %w[dfe-header-f-header-flex], navigation_label: 'Primary navigation')) do |header| + header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) if user_signed_in? - header.with_navigation_item(text: 'My modules', href: my_modules_path) - header.with_navigation_item(text: 'Learning log', href: user_notes_path) if current_user.course_started? - header.with_navigation_item(text: 'My account', href: user_path) - header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }) + header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) + header.with_action_link(text: 'Sign out', href: destroy_user_session_path, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) + header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) + header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? + header.with_navigation_item(text: 'My account', href: user_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) + header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) else - header.with_navigation_item(text: 'Sign in', href: new_user_session_path) + header.with_action_link(text: 'Sign in', href: new_user_session_path, options: { inverse: true }) + header.with_navigation_item(text: 'Sign in', href: new_user_session_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) end end end - # @return [String] - def custom_logo - [ - image_tag('crest.png', alt: 'Department for Education homepage', class: 'govuk-header__logotype-crown-fallback-image'), - content_tag(:span, 'Department for Education | ', class: 'govuk-header__logotype-text'), - content_tag(:span, service_name, class: 'govuk-header__product-name'), - ].join.html_safe - end - - CONFIG_SUMMARY = [ - ['Rails version', Rails.version], - ['Ruby version', RUBY_VERSION], - ['GOV.UK Frontend', JSON.parse(File.read(Rails.root.join('package.json'))).dig('dependencies', 'govuk-frontend').tr('^', '')], - ].freeze - # @return [String] def configuration_summary_list rows = diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 4af428848..0dc2c1a00 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -12,8 +12,7 @@ def login_uri state: state, } - uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/authorize") - uri.query = URI.encode_www_form(params) + uri = uri_constructor('authorize', params) "#{uri}&redirect_uri=#{ENV['GOV_ONE_REDIRECT_URI']}" end @@ -24,8 +23,15 @@ def logout_uri state: SecureRandom.uuid, } - uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/logout") - uri.query = URI.encode_www_form(params) + uri = uri_constructor('logout', params) "#{uri}&post_logout_redirect_uri=#{ENV['GOV_ONE_LOGOUT_REDIRECT_URI']}" end + +private + + def uri_constructor(endpoint, params) + uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/#{endpoint}") + uri.query = URI.encode_www_form(params) + uri + end end diff --git a/app/models/user.rb b/app/models/user.rb index eeffbf589..d05fad376 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,10 +24,17 @@ def self.dashboard_headers # @param email [String] # @param gov_one_id [String] # @return [User] - def self.create_from_gov_one(email:, gov_one_id:) - user = new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) - user.save!(validate: false) - user + def self.find_or_create_from_gov_one(email:, gov_one_id:) + existing_user = find_by(email: email) || find_by(gov_one_id: gov_one_id) + + if existing_user + existing_user.update!(email: email) + existing_user.update!(gov_one_id: gov_one_id) if existing_user.gov_one_id.nil? + else + existing_user = new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) + existing_user.save!(validate: false) + end + existing_user end # Include default devise modules. Others available are: diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index bfda10fd8..eda7cb1d0 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -5,6 +5,8 @@ class GovOneAuthService option :code, Types::String + BASE_URL = ENV['GOV_ONE_BASE_URI'].freeze + # @return [Hash] def tokens http = build_http(token_uri) @@ -47,11 +49,11 @@ def decode_id_token(token) # @return [URI] def token_uri - URI.parse("#{ENV['GOV_ONE_BASE_URI']}/token") + URI.parse("#{BASE_URL}/token") end def userinfo_uri - URI.parse("#{ENV['GOV_ONE_BASE_URI']}/userinfo") + URI.parse("#{BASE_URL}/userinfo") end # @param uri [URI] @@ -64,7 +66,7 @@ def build_http(uri) # @return [Hash] def jwks - discovery_url = "#{ENV['GOV_ONE_BASE_URI']}/.well-known/jwks.json" + discovery_url = "#{BASE_URL}/.well-known/jwks.json" uri = URI.parse(discovery_url) http = build_http(uri) response = http.request(Net::HTTP::Get.new(uri.path)) @@ -76,7 +78,7 @@ def jwt_assertion rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) payload = { - aud: "#{ENV['GOV_ONE_BASE_URI']}/token", + aud: "#{BASE_URL}/token", iss: Rails.application.config.gov_one_client_id, sub: Rails.application.config.gov_one_client_id, exp: Time.zone.now.to_i + 5 * 60, @@ -91,7 +93,7 @@ def jwt_assertion def token_body { grant_type: 'authorization_code', - code: @code, + code: code, redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', client_assertion: jwt_assertion, diff --git a/config/application.rb b/config/application.rb index 08b3eb4b1..ab2578476 100644 --- a/config/application.rb +++ b/config/application.rb @@ -111,7 +111,7 @@ def debug? end # @return [Boolean] - def gov_one_login_enabled? + def gov_one_login? Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN_ENABLED', false)] end diff --git a/config/routes.rb b/config/routes.rb index 289c4f67a..0dc304b67 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview - get 'gov-one/info', to: 'gov_one#info' if Rails.application.gov_one_login_enabled? + get 'gov-one/info', to: 'gov_one#info' if Rails.application.gov_one_login? get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all From ceaa9ddff65713e337822b842d91ec7e04a75e75 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 9 Nov 2023 15:12:16 +0000 Subject: [PATCH 029/122] add gov one documentation and refactor service --- .env.example | 2 +- .../users/omniauth_callbacks_controller.rb | 2 ++ app/services/gov_one_auth_service.rb | 24 +++++++++++++------ config/application.rb | 5 +++- spec/services/gov_one_auth_service_spec.rb | 5 ++-- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index e7f331853..380f3789d 100644 --- a/.env.example +++ b/.env.example @@ -82,7 +82,7 @@ E2E=true # Deployment environment ENVIRONMENT= -GOV_ONE_LOGIN_ENABLED=true +GOV_ONE_LOGIN=true # Base URI for Gov One Login requests GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 6a1b2b04f..bed63b459 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -1,4 +1,6 @@ # Controller handling OmniAuth callbacks for user authentication. +# This controller uses the GovOneAuthService to retrieve user informaton and create or sign in an user based on the email address or gov one id + class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # This method is called by Devise after successful Gov One Login authentication # @return [nil] diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index eda7cb1d0..298983b7b 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -1,12 +1,17 @@ # Service for interacting with Gov One Login +# This service is initialised with an authorisation code and can be used to: +# - exchange an authorisation code for tokens (access and id) +# - exchange an access token for user info +# - decode an id token to get the user's gov one id +# +# More information on the Gov One Login integration environment can be found here: +# https://docs.sign-in.service.gov.uk/integrate-with-integration-environment/ class GovOneAuthService extend Dry::Initializer option :code, Types::String - BASE_URL = ENV['GOV_ONE_BASE_URI'].freeze - # @return [Hash] def tokens http = build_http(token_uri) @@ -49,11 +54,17 @@ def decode_id_token(token) # @return [URI] def token_uri - URI.parse("#{BASE_URL}/token") + gov_one_uri('token') end + # @return [URI] def userinfo_uri - URI.parse("#{BASE_URL}/userinfo") + gov_one_uri('userinfo') + end + + # @return [URI] + def gov_one_uri(endpoint) + URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") end # @param uri [URI] @@ -66,8 +77,7 @@ def build_http(uri) # @return [Hash] def jwks - discovery_url = "#{BASE_URL}/.well-known/jwks.json" - uri = URI.parse(discovery_url) + uri = gov_one_uri('.well-known/jwks.json') http = build_http(uri) response = http.request(Net::HTTP::Get.new(uri.path)) JSON.parse(response.body) @@ -78,7 +88,7 @@ def jwt_assertion rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) payload = { - aud: "#{BASE_URL}/token", + aud: token_uri.to_s, iss: Rails.application.config.gov_one_client_id, sub: Rails.application.config.gov_one_client_id, exp: Time.zone.now.to_i + 5 * 60, diff --git a/config/application.rb b/config/application.rb index ab2578476..02cdc1841 100644 --- a/config/application.rb +++ b/config/application.rb @@ -69,6 +69,9 @@ class Application < Rails::Application config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) # Gov one + config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI', '#GOV_ONE_BASE_URI_env_var_missing') + config.gov_one_redirect_uri = ENV.fetch('GOV_ONE_REDIRECT_URI', '#GOV_ONE_REDIRECT_URI_env_var_missing') + config.gov_one_logout_redirect_uri = ENV.fetch('GOV_ONE_LOGOUT_REDIRECT_URI', '#GOV_ONE_LOGOUT_REDIRECT_URI_env_var_missing') config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) @@ -112,7 +115,7 @@ def debug? # @return [Boolean] def gov_one_login? - Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN_ENABLED', false)] + Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] end # @return [Boolean] diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 7cd6cb351..b686eea34 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -7,9 +7,8 @@ let(:mock_http) { instance_double('Sentry::Net::HTTP') } before do - allow(ENV).to receive(:[]).and_call_original - allow(ENV).to receive(:[]).with('GOV_ONE_REDIRECT_URI').and_return('mock_redirect_uri') - allow(ENV).to receive(:[]).with('GOV_ONE_BASE_URI').and_return('https://example.com') + allow(Rails.application.config).to receive(:gov_one_base_uri).and_return('https://example.com') + allow(Rails.application.config).to receive(:gov_one_redirect_uri).and_return('mock_redirect_uri') allow(mock_http).to receive(:request).and_return(mock_response) allow(Net::HTTP).to receive(:new).and_return(mock_http) end From 60597a709f1b3640f3c2d7ea02e8a8f996e04316 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 9 Nov 2023 15:33:31 +0000 Subject: [PATCH 030/122] refactor gov one helper and update yard --- app/helpers/gov_one_helper.rb | 13 ++++++++----- app/services/gov_one_auth_service.rb | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 0dc2c1a00..b4945c0d6 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -12,8 +12,8 @@ def login_uri state: state, } - uri = uri_constructor('authorize', params) - "#{uri}&redirect_uri=#{ENV['GOV_ONE_REDIRECT_URI']}" + uri = gov_one_uri('authorize', params) + "#{uri}&redirect_uri=#{Rails.application.config.gov_one_redirect_uri}" end # @return [String] @@ -21,15 +21,18 @@ def logout_uri params = { id_token_hint: session[:id_token], state: SecureRandom.uuid, + post_logout_redirect_uri: Rails.application.config.gov_one_logout_redirect_uri, } - uri = uri_constructor('logout', params) - "#{uri}&post_logout_redirect_uri=#{ENV['GOV_ONE_LOGOUT_REDIRECT_URI']}" + uri = gov_one_uri('logout', params) + "#{uri}&post_logout_redirect_uri=#{Rails.application.config.gov_one_logout_redirect_uri}" end private - def uri_constructor(endpoint, params) +# @param endpoint [String] the gov one endpoint +# @param params [Hash] query params + def gov_one_uri(endpoint, params) uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/#{endpoint}") uri.query = URI.encode_www_form(params) uri diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 298983b7b..93029da85 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -62,6 +62,7 @@ def userinfo_uri gov_one_uri('userinfo') end + # @param endpoint [String] # @return [URI] def gov_one_uri(endpoint) URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") From 368e8ab96ec36221d6772212ba5381ad377170f4 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 9 Nov 2023 15:35:11 +0000 Subject: [PATCH 031/122] rubocop --- app/helpers/gov_one_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index b4945c0d6..06a614065 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -30,8 +30,8 @@ def logout_uri private -# @param endpoint [String] the gov one endpoint -# @param params [Hash] query params + # @param endpoint [String] the gov one endpoint + # @param params [Hash] query params def gov_one_uri(endpoint, params) uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/#{endpoint}") uri.query = URI.encode_www_form(params) From d297b42eaadec941d65376a5584f9f28b9e2ea87 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 10 Nov 2023 10:22:27 +0000 Subject: [PATCH 032/122] update gov one auth service spec --- spec/services/gov_one_auth_service_spec.rb | 48 ++++++++++------------ 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index b686eea34..e4e0ea4f8 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -1,5 +1,19 @@ require 'rails_helper' +RSpec.shared_examples 'an unsuccessful request' do + it 'returns an empty hash' do + allow(mock_response).to receive(:body).and_return({}.to_json) + expect(result).to eq({}) + end +end + +RSpec.shared_examples 'a successful request' do |payload| + it 'returns a hash of user data' do + allow(mock_response).to receive(:body).and_return(payload.to_json) + expect(result).to eq(payload) + end +end + RSpec.describe GovOneAuthService do let(:code) { 'mock_code' } let(:mock_response) { instance_double('response') } @@ -14,47 +28,29 @@ end describe '#tokens' do - let(:token_payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } + let(:payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } + let(:result) { auth_service.tokens } context 'when the request is successful' do - it 'returns a hash of tokens' do - allow(mock_response).to receive(:body).and_return(token_payload.to_json) - - result = auth_service.tokens - expect(result).to eq(token_payload) - end + it_behaves_like 'a successful request' end context 'when the request is unsuccessful' do - it 'returns an empty hash' do - allow(mock_response).to receive(:body).and_return({}.to_json) - - result = auth_service.tokens - expect(result).to eq({}) - end + it_behaves_like 'an unsuccessful request' end end describe '#user_info' do let(:access_token) { 'mock_access_token' } - let(:user_info_payload) { { 'email' => 'test@test.com' } } + let(:payload) { { 'email' => 'test@test.com' } } + let(:result) { auth_service.user_info(access_token) } context 'when the request is successful' do - it 'returns a hash of user info' do - allow(mock_response).to receive(:body).and_return(user_info_payload.to_json) - - result = auth_service.user_info(access_token) - expect(result).to eq(user_info_payload) - end + it_behaves_like 'a successful request' end context 'when the request is unsuccessful' do - it 'returns an empty hash' do - allow(mock_response).to receive(:body).and_return({}.to_json) - - result = auth_service.user_info(access_token) - expect(result).to eq({}) - end + it_behaves_like 'an unsuccessful request' end end end From 7294ab73601bba2594c50ed4a4e2e664298b9593 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 10 Nov 2023 10:26:38 +0000 Subject: [PATCH 033/122] update devise config --- config/initializers/devise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 050e97991..6ff37f9ac 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -313,7 +313,7 @@ def i18n_message(default = nil) client_options: { port: 443, scheme: 'https', - host: 'oidc.integration.account.gov.uk', + host: Rails.application.config.gov_one_base_uri, identifier: Rails.application.config.gov_one_client_id, redirect_uri: 'users/auth/openid_connect/callback', }, From 1eaf6ed159dca7ec6d3f1ede8293ba892b5d8647 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 10 Nov 2023 13:20:16 +0000 Subject: [PATCH 034/122] update gov one helper and spec --- app/helpers/gov_one_helper.rb | 11 +++++-- spec/helpers/gov_one_helper_spec.rb | 47 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 spec/helpers/gov_one_helper_spec.rb diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 06a614065..837d4fb96 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -2,7 +2,7 @@ module GovOneHelper # @return [String] def login_uri state = SecureRandom.uuid - session[:gov_one_auth_state] = state + session[:gov_one_auth_state] = state unless Rails.env.test? params = { response_type: 'code', @@ -19,7 +19,7 @@ def login_uri # @return [String] def logout_uri params = { - id_token_hint: session[:id_token], + id_token_hint: current_id_token, state: SecureRandom.uuid, post_logout_redirect_uri: Rails.application.config.gov_one_logout_redirect_uri, } @@ -30,10 +30,15 @@ def logout_uri private + # @return [String] + def current_id_token + session[:id_token] + end + # @param endpoint [String] the gov one endpoint # @param params [Hash] query params def gov_one_uri(endpoint, params) - uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/#{endpoint}") + uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") uri.query = URI.encode_www_form(params) uri end diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb new file mode 100644 index 000000000..a3973a0a5 --- /dev/null +++ b/spec/helpers/gov_one_helper_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +RSpec.describe GovOneHelper do + let(:class_instance) { Class.new { include GovOneHelper }.new } + + before do + allow(Rails.application.config).to receive(:gov_one_base_uri).and_return('https://example.com') + allow(Rails.application.config).to receive(:gov_one_redirect_uri).and_return('https://example.com/redirect') + allow(Rails.application.config).to receive(:gov_one_logout_redirect_uri).and_return('https://example.com/logout-redirect') + allow(Rails.application.config).to receive(:gov_one_client_id).and_return('client_id') + end + + describe '#gov_one_uri' do + it 'constructs the URI with correct parameters' do + endpoint = 'authorize' + params = { response_type: 'code', scope: 'email openid', client_id: 'client_id' } + + uri = class_instance.send(:gov_one_uri, endpoint, params) + + expect(uri.to_s).to eq('https://example.com/authorize?response_type=code&scope=email+openid&client_id=client_id') + end + end + + describe '#login_uri' do + it 'constructs the URI with correct parameters' do + result = class_instance.login_uri + uri = URI.parse(result) + + expect(uri.to_s).to include('https://example.com/authorize') + expect(uri.query).to include('response_type=code', 'scope=email+openid', 'client_id=client_id', 'redirect_uri=https://example.com/redirect') + end + end + + describe '#logout_uri' do + before do + allow(class_instance).to receive(:current_id_token).and_return('mock_id_token') + end + + it 'constructs the URI with correct parameters' do + result = class_instance.logout_uri + uri = URI.parse(result) + + expect(uri.to_s).to include('https://example.com/logout') + expect(uri.query).to include('id_token_hint=mock_id_token', 'state=', 'post_logout_redirect_uri=https://example.com/logout-redirect') + end + end +end From 6ca601a61f834526fbf90020db7e6c79b96683d3 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Fri, 10 Nov 2023 14:28:25 +0000 Subject: [PATCH 035/122] Helper methods with encoded URIs and specs --- README.md | 5 ++++ app/helpers/gov_one_helper.rb | 28 ++++++++----------- docker-compose.test.yml | 3 ++ spec/helpers/gov_one_helper_spec.rb | 43 ++++++----------------------- spec/lib/seed_snippets_spec.rb | 2 +- 5 files changed, 29 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index bdbe02141..e0f454480 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,11 @@ The status of GovUK notify can be checked here: <https://status.notifications.se For more information the Notify team can be contacted here: <https://www.notifications.service.gov.uk/support>, or in the UK Government digital slack workspace in the `#govuk-notify` channel. +--- + +# One Login + +<https://signin.integration.account.gov.uk/sign-in-or-create> --- diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 837d4fb96..488e8d8fc 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -1,44 +1,38 @@ module GovOneHelper # @return [String] def login_uri - state = SecureRandom.uuid - session[:gov_one_auth_state] = state unless Rails.env.test? - params = { response_type: 'code', scope: 'email openid', client_id: Rails.application.config.gov_one_client_id, nonce: SecureRandom.uuid, - state: state, + state: SecureRandom.uuid, + redirect_uri: Rails.application.config.gov_one_redirect_uri, } - uri = gov_one_uri('authorize', params) - "#{uri}&redirect_uri=#{Rails.application.config.gov_one_redirect_uri}" + session[:gov_one_auth_state] = params[:state] + + gov_one_uri('authorize', params).to_s end # @return [String] def logout_uri params = { - id_token_hint: current_id_token, + id_token_hint: session[:id_token], state: SecureRandom.uuid, post_logout_redirect_uri: Rails.application.config.gov_one_logout_redirect_uri, } - uri = gov_one_uri('logout', params) - "#{uri}&post_logout_redirect_uri=#{Rails.application.config.gov_one_logout_redirect_uri}" + gov_one_uri('logout', params).to_s end private - # @return [String] - def current_id_token - session[:id_token] - end - - # @param endpoint [String] the gov one endpoint - # @param params [Hash] query params + # @param endpoint [String] + # @param params [Hash] + # @return [URI::HTTP, URI::HTTPS] def gov_one_uri(endpoint, params) - uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") + uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/#{endpoint}") uri.query = URI.encode_www_form(params) uri end diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 09abac58e..7b61fe942 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -14,5 +14,8 @@ services: - DEBUG=false - BOT_TOKEN=bot_token - TIMEOUT_MINUTES=25 + - GOV_ONE_BASE_URI=https://oidc.test.account.gov.uk + - GOV_ONE_REDIRECT_URI=http://recovery.app/users/auth/openid_connect/callback + - GOV_ONE_LOGOUT_REDIRECT_URI=http://recovery.app/users/sign_out tty: true stdin_open: true diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index a3973a0a5..196178d2f 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -1,47 +1,22 @@ require 'rails_helper' -RSpec.describe GovOneHelper do - let(:class_instance) { Class.new { include GovOneHelper }.new } - - before do - allow(Rails.application.config).to receive(:gov_one_base_uri).and_return('https://example.com') - allow(Rails.application.config).to receive(:gov_one_redirect_uri).and_return('https://example.com/redirect') - allow(Rails.application.config).to receive(:gov_one_logout_redirect_uri).and_return('https://example.com/logout-redirect') - allow(Rails.application.config).to receive(:gov_one_client_id).and_return('client_id') - end - - describe '#gov_one_uri' do - it 'constructs the URI with correct parameters' do - endpoint = 'authorize' - params = { response_type: 'code', scope: 'email openid', client_id: 'client_id' } - - uri = class_instance.send(:gov_one_uri, endpoint, params) - - expect(uri.to_s).to eq('https://example.com/authorize?response_type=code&scope=email+openid&client_id=client_id') - end - end +describe 'GovOneHelper', type: :helper do describe '#login_uri' do - it 'constructs the URI with correct parameters' do - result = class_instance.login_uri - uri = URI.parse(result) + subject(:login_uri) { helper.login_uri } - expect(uri.to_s).to include('https://example.com/authorize') - expect(uri.query).to include('response_type=code', 'scope=email+openid', 'client_id=client_id', 'redirect_uri=https://example.com/redirect') + it 'encodes the authorize endpoint params' do + expect(login_uri).to start_with 'https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=' + expect(login_uri).to end_with 'redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' end end describe '#logout_uri' do - before do - allow(class_instance).to receive(:current_id_token).and_return('mock_id_token') - end - - it 'constructs the URI with correct parameters' do - result = class_instance.logout_uri - uri = URI.parse(result) + subject(:logout_uri) { helper.logout_uri } - expect(uri.to_s).to include('https://example.com/logout') - expect(uri.query).to include('id_token_hint=mock_id_token', 'state=', 'post_logout_redirect_uri=https://example.com/logout-redirect') + it 'encodes the logout endpoint params' do + expect(logout_uri).to start_with 'https://oidc.test.account.gov.uk/logout?id_token_hint&state=' + expect(logout_uri).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' end end end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 8add68d26..89e91259b 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 177 + expect(locales.count).to be 181 end it 'dot separated key -> Page::Resource#name' do From d95d246653cbea0b5e23776d4114f961fef40abb Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Fri, 10 Nov 2023 14:53:22 +0000 Subject: [PATCH 036/122] Update CI envs --- .github/workflows/ci.yml | 3 +++ app/models/user.rb | 14 ++++++-------- spec/helpers/gov_one_helper_spec.rb | 1 - 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0b10efcf..1cea13948 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token + GOV_ONE_BASE_URI: https://oidc.test.account.gov.uk + GOV_ONE_REDIRECT_URI: https://recovery.app/users/auth/openid_connect/callback + GOV_ONE_LOGOUT_REDIRECT_URI: https://recovery.app/users/sign_out services: postgres: diff --git a/app/models/user.rb b/app/models/user.rb index d05fad376..711c392a3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,16 +25,14 @@ def self.dashboard_headers # @param gov_one_id [String] # @return [User] def self.find_or_create_from_gov_one(email:, gov_one_id:) - existing_user = find_by(email: email) || find_by(gov_one_id: gov_one_id) - - if existing_user - existing_user.update!(email: email) - existing_user.update!(gov_one_id: gov_one_id) if existing_user.gov_one_id.nil? + if (user = find_by(email: email) || find_by(gov_one_id: gov_one_id)) + user.update!(email: email) + user.update!(gov_one_id: gov_one_id) if user.gov_one_id.nil? else - existing_user = new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) - existing_user.save!(validate: false) + user = new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) + user.save!(validate: false) end - existing_user + user end # Include default devise modules. Others available are: diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index 196178d2f..27994974d 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' describe 'GovOneHelper', type: :helper do - describe '#login_uri' do subject(:login_uri) { helper.login_uri } From 974be47b7c52908de25116e3acb1e1b46e278fae Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 10 Nov 2023 16:49:15 +0000 Subject: [PATCH 037/122] refactor gov one service and helper --- .github/workflows/ci.yml | 3 +++ app/helpers/gov_one_helper.rb | 8 ++++---- app/services/gov_one_auth_service.rb | 21 +++++++++++---------- app/views/gov_one/info.html.slim | 4 ++-- docker-compose.test.yml | 3 +++ spec/helpers/gov_one_helper_spec.rb | 21 ++++++++------------- spec/services/gov_one_auth_service_spec.rb | 4 +--- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cc1a306a..58606bf0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token + GOV_ONE_CLIENT_ID: client_id + GOV_ONE_BASE_URI: https://oidc.integration.account.gov.uk + GOV_ONE_REDIRECT_URI: mock_redirect_uri services: postgres: diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 837d4fb96..ce25e546b 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -10,10 +10,10 @@ def login_uri client_id: Rails.application.config.gov_one_client_id, nonce: SecureRandom.uuid, state: state, + redirect_uri: Rails.application.config.gov_one_redirect_uri, } - uri = gov_one_uri('authorize', params) - "#{uri}&redirect_uri=#{Rails.application.config.gov_one_redirect_uri}" + gov_one_uri('authorize', params) end # @return [String] @@ -24,8 +24,7 @@ def logout_uri post_logout_redirect_uri: Rails.application.config.gov_one_logout_redirect_uri, } - uri = gov_one_uri('logout', params) - "#{uri}&post_logout_redirect_uri=#{Rails.application.config.gov_one_logout_redirect_uri}" + gov_one_uri('logout', params) end private @@ -37,6 +36,7 @@ def current_id_token # @param endpoint [String] the gov one endpoint # @param params [Hash] query params + # @return [URI] def gov_one_uri(endpoint, params) uri = URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") uri.query = URI.encode_www_form(params) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 93029da85..cd5aa4f6e 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -4,14 +4,19 @@ # - exchange an access token for user info # - decode an id token to get the user's gov one id # -# More information on the Gov One Login integration environment can be found here: -# https://docs.sign-in.service.gov.uk/integrate-with-integration-environment/ +# @see https://docs.sign-in.service.gov.uk/integrate-with-integration-environment/ class GovOneAuthService extend Dry::Initializer option :code, Types::String + GOV_ONE_URIS = { + token: '/token', + userinfo: '/userinfo', + jwk: '/.well-known/jwks.json', + }.freeze + # @return [Hash] def tokens http = build_http(token_uri) @@ -29,6 +34,7 @@ def tokens # @param access_token [String] # @return [Hash] def user_info(access_token) + userinfo_uri = gov_one_uri(GOV_ONE_URIS[:userinfo]) http = build_http(userinfo_uri) userinfo_request = Net::HTTP::Get.new(userinfo_uri.path, { 'Authorization' => "Bearer #{access_token}" }) @@ -54,18 +60,13 @@ def decode_id_token(token) # @return [URI] def token_uri - gov_one_uri('token') - end - - # @return [URI] - def userinfo_uri - gov_one_uri('userinfo') + gov_one_uri(GOV_ONE_URIS[:token]) end # @param endpoint [String] # @return [URI] def gov_one_uri(endpoint) - URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") + URI.join(Rails.application.config.gov_one_base_uri, endpoint) end # @param uri [URI] @@ -78,7 +79,7 @@ def build_http(uri) # @return [Hash] def jwks - uri = gov_one_uri('.well-known/jwks.json') + uri = gov_one_uri(GOV_ONE_URIS[:jwk]) http = build_http(uri) response = http.request(Net::HTTP::Get.new(uri.path)) JSON.parse(response.body) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index da7a2602f..39951ca0e 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -8,7 +8,7 @@ = m('gov-one-info.body') - if current_user - = govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri + = govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri.to_s - else - = govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri + = govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri.to_s \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 09abac58e..b1ff4aaf8 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -14,5 +14,8 @@ services: - DEBUG=false - BOT_TOKEN=bot_token - TIMEOUT_MINUTES=25 + - GOV_ONE_CLIENT_ID=client_id + - GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk + - GOV_ONE_REDIRECT_URI=mock_redirect_uri tty: true stdin_open: true diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index a3973a0a5..842851f95 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -4,10 +4,7 @@ let(:class_instance) { Class.new { include GovOneHelper }.new } before do - allow(Rails.application.config).to receive(:gov_one_base_uri).and_return('https://example.com') - allow(Rails.application.config).to receive(:gov_one_redirect_uri).and_return('https://example.com/redirect') - allow(Rails.application.config).to receive(:gov_one_logout_redirect_uri).and_return('https://example.com/logout-redirect') - allow(Rails.application.config).to receive(:gov_one_client_id).and_return('client_id') + allow(Rails.application.config).to receive(:gov_one_logout_redirect_uri).and_return('mock_logout_redirect_uri') end describe '#gov_one_uri' do @@ -17,17 +14,16 @@ uri = class_instance.send(:gov_one_uri, endpoint, params) - expect(uri.to_s).to eq('https://example.com/authorize?response_type=code&scope=email+openid&client_id=client_id') + expect(uri.to_s).to eq('https://oidc.integration.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=client_id') end end describe '#login_uri' do it 'constructs the URI with correct parameters' do - result = class_instance.login_uri - uri = URI.parse(result) + uri = class_instance.login_uri - expect(uri.to_s).to include('https://example.com/authorize') - expect(uri.query).to include('response_type=code', 'scope=email+openid', 'client_id=client_id', 'redirect_uri=https://example.com/redirect') + expect(uri.to_s).to include('https://oidc.integration.account.gov.uk/authorize') + expect(uri.query).to include('response_type=code', 'scope=email+openid', 'client_id=client_id', 'redirect_uri=mock_redirect_uri') end end @@ -37,11 +33,10 @@ end it 'constructs the URI with correct parameters' do - result = class_instance.logout_uri - uri = URI.parse(result) + uri = class_instance.logout_uri - expect(uri.to_s).to include('https://example.com/logout') - expect(uri.query).to include('id_token_hint=mock_id_token', 'state=', 'post_logout_redirect_uri=https://example.com/logout-redirect') + expect(uri.to_s).to include('https://oidc.integration.account.gov.uk/logout') + expect(uri.query).to include('id_token_hint=mock_id_token', 'state=', 'post_logout_redirect_uri=mock_logout_redirect_uri') end end end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index e4e0ea4f8..b416d83a7 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -7,7 +7,7 @@ end end -RSpec.shared_examples 'a successful request' do |payload| +RSpec.shared_examples 'a successful request' do it 'returns a hash of user data' do allow(mock_response).to receive(:body).and_return(payload.to_json) expect(result).to eq(payload) @@ -21,8 +21,6 @@ let(:mock_http) { instance_double('Sentry::Net::HTTP') } before do - allow(Rails.application.config).to receive(:gov_one_base_uri).and_return('https://example.com') - allow(Rails.application.config).to receive(:gov_one_redirect_uri).and_return('mock_redirect_uri') allow(mock_http).to receive(:request).and_return(mock_response) allow(Net::HTTP).to receive(:new).and_return(mock_http) end From bc7ddbedeff6887530a00c09605d31009deaf4dc Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Fri, 10 Nov 2023 17:26:57 +0000 Subject: [PATCH 038/122] Refactor to use a constant and reduce number of variables --- .env.example | 7 +-- .github/workflows/ci.yml | 2 - app/helpers/gov_one_helper.rb | 12 ++--- app/services/gov_one_auth_service.rb | 68 +++++++++++++--------------- app/views/gov_one/info.html.slim | 8 ++-- config/application.rb | 10 ++-- config/locales/en.yml | 18 ++++---- config/sitemap.rb | 3 +- docker-compose.dev.yml | 7 +-- docker-compose.test.yml | 2 - 10 files changed, 60 insertions(+), 77 deletions(-) diff --git a/.env.example b/.env.example index 380f3789d..ae0b13f45 100644 --- a/.env.example +++ b/.env.example @@ -82,15 +82,10 @@ E2E=true # Deployment environment ENVIRONMENT= +# Gov One Authentication GOV_ONE_LOGIN=true - -# Base URI for Gov One Login requests GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk -# Redirect URIs for Gov One Login redirects -# Replace localhost:3000 with the correct application root path -GOV_ONE_REDIRECT_URI=http://localhost:3000/users/auth/openid_connect/callback -GOV_ONE_LOGOUT_REDIRECT_URI=http://localhost:3000/users/sign_out # DISPLAY SERVICE_UNAVAILABLE PAGE MAINTENANCE = true \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cea13948..919fd8ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,6 @@ jobs: DOMAIN: recovery.app BOT_TOKEN: bot_token GOV_ONE_BASE_URI: https://oidc.test.account.gov.uk - GOV_ONE_REDIRECT_URI: https://recovery.app/users/auth/openid_connect/callback - GOV_ONE_LOGOUT_REDIRECT_URI: https://recovery.app/users/sign_out services: postgres: diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 488e8d8fc..575622145 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -7,12 +7,12 @@ def login_uri client_id: Rails.application.config.gov_one_client_id, nonce: SecureRandom.uuid, state: SecureRandom.uuid, - redirect_uri: Rails.application.config.gov_one_redirect_uri, + redirect_uri: GovOneAuthService::CALLBACKS[:login], } session[:gov_one_auth_state] = params[:state] - gov_one_uri('authorize', params).to_s + gov_one_uri(:login, params).to_s end # @return [String] @@ -20,19 +20,19 @@ def logout_uri params = { id_token_hint: session[:id_token], state: SecureRandom.uuid, - post_logout_redirect_uri: Rails.application.config.gov_one_logout_redirect_uri, + post_logout_redirect_uri: GovOneAuthService::CALLBACKS[:logout], } - gov_one_uri('logout', params).to_s + gov_one_uri(:logout, params).to_s end private - # @param endpoint [String] + # @param endpoint [Symbol] # @param params [Hash] # @return [URI::HTTP, URI::HTTPS] def gov_one_uri(endpoint, params) - uri = URI.parse("#{ENV['GOV_ONE_BASE_URI']}/#{endpoint}") + uri = URI.parse(GovOneAuthService::ENDPOINTS[endpoint]) uri.query = URI.encode_www_form(params) uri end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 93029da85..e806fc691 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -1,22 +1,34 @@ -# Service for interacting with Gov One Login -# This service is initialised with an authorisation code and can be used to: +# # - exchange an authorisation code for tokens (access and id) # - exchange an access token for user info # - decode an id token to get the user's gov one id # -# More information on the Gov One Login integration environment can be found here: -# https://docs.sign-in.service.gov.uk/integrate-with-integration-environment/ - +# @see https://docs.sign-in.service.gov.uk/ class GovOneAuthService + # @return [Hash{Symbol => String}] + CALLBACKS = { + login: "#{Rails.application.config.service_url}/users/auth/openid_connect/callback", + logout: "#{Rails.application.config.service_url}/users/sign_out", + }.freeze + + # @return [Hash{Symbol => String}] + ENDPOINTS = { + login: "#{Rails.application.config.gov_one_base_uri}/authorize", + logout: "#{Rails.application.config.gov_one_base_uri}/logout", + token: "#{Rails.application.config.gov_one_base_uri}/token", + userinfo: "#{Rails.application.config.gov_one_base_uri}/userinfo", + jwks: "#{Rails.application.config.gov_one_base_uri}/.well-known/jwks.json", + }.freeze + extend Dry::Initializer - option :code, Types::String + option :code, Types::Strict::String + # POST /token # @return [Hash] def tokens - http = build_http(token_uri) - - token_request = Net::HTTP::Post.new(token_uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) + uri, http = build_http(ENDPOINTS[:token]) + token_request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) token_request.set_form_data(token_body) token_response = http.request(token_request) @@ -26,12 +38,12 @@ def tokens {} end + # GET /userinfo # @param access_token [String] # @return [Hash] def user_info(access_token) - http = build_http(userinfo_uri) - - userinfo_request = Net::HTTP::Get.new(userinfo_uri.path, { 'Authorization' => "Bearer #{access_token}" }) + uri, http = build_http(ENDPOINTS[:userinfo]) + userinfo_request = Net::HTTP::Get.new(uri.path, { 'Authorization' => "Bearer #{access_token}" }) userinfo_response = http.request(userinfo_request) JSON.parse(userinfo_response.body) @@ -52,34 +64,18 @@ def decode_id_token(token) private - # @return [URI] - def token_uri - gov_one_uri('token') - end - - # @return [URI] - def userinfo_uri - gov_one_uri('userinfo') - end - - # @param endpoint [String] - # @return [URI] - def gov_one_uri(endpoint) - URI.parse("#{Rails.application.config.gov_one_base_uri}/#{endpoint}") - end - - # @param uri [URI] - # @return [Net::HTTP] - def build_http(uri) + # @param address [String] + # @return [Array<URI::HTTP, Net::HTTP>] + def build_http(address) + uri = URI.parse(address) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true unless Rails.env.test? - http + [uri, http] end # @return [Hash] def jwks - uri = gov_one_uri('.well-known/jwks.json') - http = build_http(uri) + uri, http = build_http(ENDPOINTS[:jwks]) response = http.request(Net::HTTP::Get.new(uri.path)) JSON.parse(response.body) end @@ -89,7 +85,7 @@ def jwt_assertion rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) payload = { - aud: token_uri.to_s, + aud: ENDPOINTS[:token], iss: Rails.application.config.gov_one_client_id, sub: Rails.application.config.gov_one_client_id, exp: Time.zone.now.to_i + 5 * 60, @@ -105,7 +101,7 @@ def token_body { grant_type: 'authorization_code', code: code, - redirect_uri: ENV['GOV_ONE_REDIRECT_URI'], + redirect_uri: CALLBACKS[:login], client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', client_assertion: jwt_assertion, } diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index da7a2602f..309f98ce8 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -1,14 +1,14 @@ = render 'learning/cms_debug' - content_for :page_title do - = html_title t('gov-one.info.title') + = html_title t('gov_one.title') .govuk-grid-row .govuk-grid-column-one-half - = m('gov-one-info.body') + = m('gov_one.body') - if current_user - = govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri + = govuk_button_link_to t('gov_one.links.sign_out'), logout_uri - else - = govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri + = govuk_button_link_to t('gov_one.links.sign_in'), login_uri \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 02cdc1841..62ccd629c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -32,6 +32,8 @@ class Application < Rails::Application end config.service_name = 'Early years child development training' + config.service_url = (Rails.env.production? ? 'https://' : 'http://') + ENV.fetch('DOMAIN', 'child-development-training') + config.internal_mailbox = ENV.fetch('INTERNAL_MAILBOX', 'child-development.training@education.gov.uk') config.middleware.use Grover::Middleware config.active_record.yaml_column_permitted_classes = [Symbol] @@ -69,11 +71,9 @@ class Application < Rails::Application config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) # Gov one - config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI', '#GOV_ONE_BASE_URI_env_var_missing') - config.gov_one_redirect_uri = ENV.fetch('GOV_ONE_REDIRECT_URI', '#GOV_ONE_REDIRECT_URI_env_var_missing') - config.gov_one_logout_redirect_uri = ENV.fetch('GOV_ONE_LOGOUT_REDIRECT_URI', '#GOV_ONE_LOGOUT_REDIRECT_URI_env_var_missing') - config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) - config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) + config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI', '#GOV_ONE_BASE_URI_env_var_missing') + config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) + config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) # @return [Boolean] def live? diff --git a/config/locales/en.yml b/config/locales/en.yml index e3f7c11b4..776d69931 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -571,15 +571,15 @@ en: %{criteria} # /gov-one/info - gov-one: - info: - title: Gov One Login - body: | - # Gov One Login - - This is some information about Gov One Login... - sign-in-button: Sign in with Gov One Login - sign-out-button: Sign out of Gov One Login + gov_one: + title: Gov One Login + body: | + # Gov One Login + + This is some information about Gov One Login... + links: + sign_in: Sign in with Gov One Login + sign_out: Sign out of Gov One Login # /settings/cookie-policy cookie_policy: diff --git a/config/sitemap.rb b/config/sitemap.rb index bfdaecb7f..901358c90 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -14,8 +14,7 @@ # } # ] # -protocol = Rails.env.production? ? 'https://' : 'http://' -SitemapGenerator::Sitemap.default_host = protocol + ENV['DOMAIN'] +SitemapGenerator::Sitemap.default_host = Rails.application.config.service_url SitemapGenerator::Sitemap.compress = false # Run this command to update /public/sitemap.xml diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6f104f3b5..8e3a4eeed 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,12 +19,9 @@ services: # app config - DATABASE_URL=postgres://postgres:password@db:5432/early_years_foundation_recovery_development - RAILS_ENV=development - - DOMAIN=app:3000 + - DOMAIN=localhost:3000 # put back to app - RAILS_SERVE_STATIC_FILES=true - # gov one login - - GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk - - GOV_ONE_REDIRECT_URI=http://localhost:3000/users/auth/openid_connect/callback - - GOV_ONE_LOGOUT_REDIRECT_URI=http://localhost:3000/users/sign_out + - GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk volumes: - .:/srv tty: true diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 7b61fe942..47551644c 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -15,7 +15,5 @@ services: - BOT_TOKEN=bot_token - TIMEOUT_MINUTES=25 - GOV_ONE_BASE_URI=https://oidc.test.account.gov.uk - - GOV_ONE_REDIRECT_URI=http://recovery.app/users/auth/openid_connect/callback - - GOV_ONE_LOGOUT_REDIRECT_URI=http://recovery.app/users/sign_out tty: true stdin_open: true From f5976812bb250a83ba83383000675303dd557e77 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 13 Nov 2023 16:22:04 +0000 Subject: [PATCH 039/122] refactor gov one auth service and specs for service and helper --- .github/workflows/ci.yml | 1 - app/helpers/gov_one_helper.rb | 4 +- app/services/gov_one_auth_service.rb | 32 +++++-- app/views/gov_one/info.html.slim | 4 +- config/application.rb | 2 +- config/credentials/development.yml.enc | 2 +- config/credentials/production.yml.enc | 2 +- config/credentials/test.yml.enc | 2 +- docker-compose.test.yml | 1 - spec/helpers/gov_one_helper_spec.rb | 18 +++- spec/services/gov_one_auth_service_spec.rb | 100 +++++++++++++++------ 11 files changed, 121 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 919fd8ca5..b0b10efcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,6 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token - GOV_ONE_BASE_URI: https://oidc.test.account.gov.uk services: postgres: diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 575622145..b9fea0c05 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -12,7 +12,7 @@ def login_uri session[:gov_one_auth_state] = params[:state] - gov_one_uri(:login, params).to_s + gov_one_uri(:login, params) end # @return [String] @@ -23,7 +23,7 @@ def logout_uri post_logout_redirect_uri: GovOneAuthService::CALLBACKS[:logout], } - gov_one_uri(:logout, params).to_s + gov_one_uri(:logout, params) end private diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index e806fc691..7f1d2809a 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -30,7 +30,7 @@ def tokens uri, http = build_http(ENDPOINTS[:token]) token_request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' }) token_request.set_form_data(token_body) - token_response = http.request(token_request) + token_response = response(token_request, http) JSON.parse(token_response.body) rescue StandardError => e @@ -44,7 +44,7 @@ def tokens def user_info(access_token) uri, http = build_http(ENDPOINTS[:userinfo]) userinfo_request = Net::HTTP::Get.new(uri.path, { 'Authorization' => "Bearer #{access_token}" }) - userinfo_response = http.request(userinfo_request) + userinfo_response = response(userinfo_request, http) JSON.parse(userinfo_response.body) rescue StandardError => e @@ -52,6 +52,20 @@ def user_info(access_token) {} end + # @param request [Net::HTTP::Get, Net::HTTP::Post] + # @param http [Net::HTTP] + # @return [Net::HTTPResponse] + def response(request, http) + http.request(request) + end + + # @param request [Net::HTTP::Get, Net::HTTP::Post] + # @return [Hash] + def handle_request(request) + response = http.request(request) + JSON.parse(response.body) + end + # @param token [String] # @return [Array<Hash>] def decode_id_token(token) @@ -62,17 +76,17 @@ def decode_id_token(token) JWT.decode(token, jwk.public_key, true, algorithm: 'ES256') end -private - # @param address [String] # @return [Array<URI::HTTP, Net::HTTP>] def build_http(address) uri = URI.parse(address) http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true unless Rails.env.test? + http.use_ssl = true [uri, http] end +private + # @return [Hash] def jwks uri, http = build_http(ENDPOINTS[:jwks]) @@ -83,8 +97,12 @@ def jwks # @return [String] def jwt_assertion rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) + JWT.encode jwt_payload, rsa_private, 'RS256' + end - payload = { + # @return [Hash] + def jwt_payload + { aud: ENDPOINTS[:token], iss: Rails.application.config.gov_one_client_id, sub: Rails.application.config.gov_one_client_id, @@ -92,8 +110,6 @@ def jwt_assertion jti: SecureRandom.uuid, iat: Time.zone.now.to_i, } - - JWT.encode payload, rsa_private, 'RS256' end # @return [Hash] diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index 309f98ce8..fce694838 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -8,7 +8,7 @@ = m('gov_one.body') - if current_user - = govuk_button_link_to t('gov_one.links.sign_out'), logout_uri + = govuk_button_link_to t('gov_one.links.sign_out'), logout_uri.to_s - else - = govuk_button_link_to t('gov_one.links.sign_in'), login_uri + = govuk_button_link_to t('gov_one.links.sign_in'), login_uri.to_s \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 62ccd629c..6eeab7a6b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -71,7 +71,7 @@ class Application < Rails::Application config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) # Gov one - config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI', '#GOV_ONE_BASE_URI_env_var_missing') + config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI', credentials.dig(:gov_one, :base_uri)) config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) diff --git a/config/credentials/development.yml.enc b/config/credentials/development.yml.enc index 4606457df..292c4ec32 100644 --- a/config/credentials/development.yml.enc +++ b/config/credentials/development.yml.enc @@ -1 +1 @@ -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--mE6HeB6Fsvd9DHxG--T8mDZYxmmz5GxmQ8ikSFTg== \ No newline at end of file +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--QhoOqS2u5TN6yGex--D+M0HrxGhh3aOh6fIxi9SQ== \ No newline at end of file diff --git a/config/credentials/production.yml.enc b/config/credentials/production.yml.enc index 9dfe3025f..0bf5120f8 100644 --- a/config/credentials/production.yml.enc +++ b/config/credentials/production.yml.enc @@ -1 +1 @@ -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--D/uaMzAQH8aFuO+u--HkOCuZK3KJsvkGlaRQ45Gg== \ No newline at end of file +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--zJGkDqYx7hBVWmDr--IjW3HIJrI8vrbQ7r4gR4Mg== \ No newline at end of file diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index e7e3894a0..69becba05 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -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--Jz3stjju1xhM5+A7--5uwznqznRm9MCJq4I4/q+w== \ No newline at end of file +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--ub0s9DBRWGcD5tT1--YSXZBp3jUhqXA6bl51La3w== \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 47551644c..09abac58e 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -14,6 +14,5 @@ services: - DEBUG=false - BOT_TOKEN=bot_token - TIMEOUT_MINUTES=25 - - GOV_ONE_BASE_URI=https://oidc.test.account.gov.uk tty: true stdin_open: true diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index 27994974d..33fe599d3 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -4,18 +4,28 @@ describe '#login_uri' do subject(:login_uri) { helper.login_uri } + it 'returns a URI object with the correct host and path' do + expect(login_uri.host).to eq 'oidc.test.account.gov.uk' + expect(login_uri.path).to eq '/authorize' + end + it 'encodes the authorize endpoint params' do - expect(login_uri).to start_with 'https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=' - expect(login_uri).to end_with 'redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' + expect(login_uri.query).to start_with 'response_type=code&scope=email+openid&client_id=' + expect(login_uri.query).to end_with '&redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' end end describe '#logout_uri' do subject(:logout_uri) { helper.logout_uri } + it 'returns a URI object with the correct host and path' do + expect(logout_uri.host).to eq 'oidc.test.account.gov.uk' + expect(logout_uri.path).to eq '/logout' + end + it 'encodes the logout endpoint params' do - expect(logout_uri).to start_with 'https://oidc.test.account.gov.uk/logout?id_token_hint&state=' - expect(logout_uri).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' + expect(logout_uri.query).to start_with 'id_token_hint&state=' + expect(logout_uri.query).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' end end end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index e4e0ea4f8..6f795b0c4 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -1,56 +1,106 @@ require 'rails_helper' -RSpec.shared_examples 'an unsuccessful request' do - it 'returns an empty hash' do - allow(mock_response).to receive(:body).and_return({}.to_json) - expect(result).to eq({}) - end -end - -RSpec.shared_examples 'a successful request' do |payload| - it 'returns a hash of user data' do - allow(mock_response).to receive(:body).and_return(payload.to_json) - expect(result).to eq(payload) - end -end - RSpec.describe GovOneAuthService do let(:code) { 'mock_code' } let(:mock_response) { instance_double('response') } let(:auth_service) { described_class.new(code: code) } - let(:mock_http) { instance_double('Sentry::Net::HTTP') } + let(:payload) { { 'info' => 'auth_info' } } before do - allow(Rails.application.config).to receive(:gov_one_base_uri).and_return('https://example.com') - allow(Rails.application.config).to receive(:gov_one_redirect_uri).and_return('mock_redirect_uri') - allow(mock_http).to receive(:request).and_return(mock_response) - allow(Net::HTTP).to receive(:new).and_return(mock_http) + allow(auth_service).to receive(:response).and_return(mock_response) end describe '#tokens' do - let(:payload) { { 'access_token' => 'mock_access_token', 'id_token' => 'mock_id_token' } } let(:result) { auth_service.tokens } context 'when the request is successful' do - it_behaves_like 'a successful request' + it 'returns a hash of tokens' do + allow(mock_response).to receive(:body).and_return(payload.to_json) + + expect(result).to eq(payload) + expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Post), an_instance_of(Net::HTTP)) + end end context 'when the request is unsuccessful' do - it_behaves_like 'an unsuccessful request' + it 'returns an empty hash' do + allow(mock_response).to receive(:body).and_return({}.to_json) + + expect(result).to eq({}) + expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Post), an_instance_of(Net::HTTP)) + end end end describe '#user_info' do let(:access_token) { 'mock_access_token' } - let(:payload) { { 'email' => 'test@test.com' } } let(:result) { auth_service.user_info(access_token) } context 'when the request is successful' do - it_behaves_like 'a successful request' + it 'returns a hash of user info' do + allow(mock_response).to receive(:body).and_return(payload.to_json) + + expect(result).to eq(payload) + expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Get), an_instance_of(Net::HTTP)) + end end context 'when the request is unsuccessful' do - it_behaves_like 'an unsuccessful request' + it 'returns an empty hash' do + allow(mock_response).to receive(:body).and_return({}.to_json) + + expect(result).to eq({}) + expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Get), an_instance_of(Net::HTTP)) + end + end + end + + describe 'CALLBACKS' do + it 'returns a frozen hash of callbacks urls' do + expect(described_class::CALLBACKS).to eq({ + login: 'http://recovery.app/users/auth/openid_connect/callback', + logout: 'http://recovery.app/users/sign_out', + }) + + expect(described_class::CALLBACKS).to be_frozen + end + end + + describe 'ENDPOINTS' do + it 'returns a frozen hash of endpoints urls' do + expect(described_class::ENDPOINTS).to eq({ + login: 'https://oidc.test.account.gov.uk/authorize', + logout: 'https://oidc.test.account.gov.uk/logout', + token: 'https://oidc.test.account.gov.uk/token', + userinfo: 'https://oidc.test.account.gov.uk/userinfo', + jwks: 'https://oidc.test.account.gov.uk/.well-known/jwks.json', + }) + + expect(described_class::ENDPOINTS).to be_frozen + end + end + + describe '#token_body' do + let(:token_body) { auth_service.send(:token_body) } + + it 'returns a hash of correct token body' do + expect(token_body[:grant_type]).to eq('authorization_code') + expect(token_body[:code]).to eq(code) + expect(token_body[:redirect_uri]).to end_with('/users/auth/openid_connect/callback') + expect(token_body[:client_assertion_type]).to eq('urn:ietf:params:oauth:client-assertion-type:jwt-bearer') + end + end + + describe '#jwt_payload' do + let(:jwt_payload) { auth_service.send(:jwt_payload) } + + it 'returns a hash of correct jwt payload' do + expect(jwt_payload[:aud]).to eq('https://oidc.test.account.gov.uk/token') + expect(jwt_payload[:iss]).to be_a(String) + expect(jwt_payload[:sub]).to be_a(String) + expect(jwt_payload[:exp]).to be_between(Time.zone.now.to_i + 4 * 60, Time.zone.now.to_i + 6 * 60) + expect(jwt_payload[:jti]).to be_a(String) + expect(jwt_payload[:iat]).to be_a(Integer) end end end From c035a0f51a9ecc57ecc33a6ac7a672ef211621b3 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 13 Nov 2023 16:34:11 +0000 Subject: [PATCH 040/122] remove typos --- app/helpers/gov_one_helper.rb | 11 ----------- app/views/gov_one/info.html.slim | 6 ------ spec/helpers/gov_one_helper_spec.rb | 8 -------- 3 files changed, 25 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 6883ba810..b9fea0c05 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -28,20 +28,9 @@ def logout_uri private -<<<<<<< HEAD - # @return [String] - def current_id_token - session[:id_token] - end - - # @param endpoint [String] the gov one endpoint - # @param params [Hash] query params - # @return [URI] -======= # @param endpoint [Symbol] # @param params [Hash] # @return [URI::HTTP, URI::HTTPS] ->>>>>>> gov-one-refactor def gov_one_uri(endpoint, params) uri = URI.parse(GovOneAuthService::ENDPOINTS[endpoint]) uri.query = URI.encode_www_form(params) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index ffb837b51..fce694838 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -8,13 +8,7 @@ = m('gov_one.body') - if current_user -<<<<<<< HEAD - = govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri.to_s - - else - = govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri.to_s -======= = govuk_button_link_to t('gov_one.links.sign_out'), logout_uri.to_s - else = govuk_button_link_to t('gov_one.links.sign_in'), login_uri.to_s ->>>>>>> gov-one-refactor \ No newline at end of file diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index 986814152..33fe599d3 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -23,17 +23,9 @@ expect(logout_uri.path).to eq '/logout' end -<<<<<<< HEAD - it 'constructs the URI with correct parameters' do - uri = class_instance.logout_uri - - expect(uri.to_s).to include('https://oidc.integration.account.gov.uk/logout') - expect(uri.query).to include('id_token_hint=mock_id_token', 'state=', 'post_logout_redirect_uri=mock_logout_redirect_uri') -======= it 'encodes the logout endpoint params' do expect(logout_uri.query).to start_with 'id_token_hint&state=' expect(logout_uri.query).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' ->>>>>>> gov-one-refactor end end end From 818839b04b01c51e13cdf975a70afb8b2c0f629d Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 13 Nov 2023 16:46:42 +0000 Subject: [PATCH 041/122] remove unneeded env variables from docker compose and ci workflow --- .github/workflows/ci.yml | 4 ---- docker-compose.test.yml | 3 --- 2 files changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71fa005ee..afcda98a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,6 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token - GOV_ONE_CLIENT_ID: client_id - GOV_ONE_BASE_URI: https://oidc.integration.account.gov.uk - GOV_ONE_REDIRECT_URI: mock_redirect_uri - services: postgres: image: postgres:15.4-alpine diff --git a/docker-compose.test.yml b/docker-compose.test.yml index b1ff4aaf8..09abac58e 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -14,8 +14,5 @@ services: - DEBUG=false - BOT_TOKEN=bot_token - TIMEOUT_MINUTES=25 - - GOV_ONE_CLIENT_ID=client_id - - GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk - - GOV_ONE_REDIRECT_URI=mock_redirect_uri tty: true stdin_open: true From 48297ce97bf727f11802c65568db7d661c2c218a Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 14 Nov 2023 10:08:14 +0000 Subject: [PATCH 042/122] enable one login by default except for production --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index 6eeab7a6b..17558d36b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -120,7 +120,7 @@ def gov_one_login? # @return [Boolean] def maintenance? - Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] + Types::Params::Bool[ENV.fetch('MAINTENANCE', true)] && !live? end # @return [ActiveSupport::TimeWithZone] From dffa92100f997363a3da07baea27ccf0102c3c85 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 14 Nov 2023 10:15:02 +0000 Subject: [PATCH 043/122] update application gov_one_login? check --- config/application.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index 17558d36b..8ea687452 100644 --- a/config/application.rb +++ b/config/application.rb @@ -115,12 +115,12 @@ def debug? # @return [Boolean] def gov_one_login? - Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] + Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', true)] && !live? end # @return [Boolean] def maintenance? - Types::Params::Bool[ENV.fetch('MAINTENANCE', true)] && !live? + Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] end # @return [ActiveSupport::TimeWithZone] From 5af4ec3e5d1df316799da3286a4c66598c7a0c60 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 14 Nov 2023 11:12:27 +0000 Subject: [PATCH 044/122] update account page for one login --- app/views/user/show.html.slim | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 1cf9f3e03..ab99e56f1 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -14,14 +14,8 @@ - row.with_key { 'Name' } - row.with_value(text: current_user.name, classes: %w[data-hj-suppress]) - row.with_action(text: 'Change name', href: edit_registration_name_path, html_attributes: { id: :edit_name_registration }) - - your_details.with_row do |row| - - row.with_key { 'Email' } - - row.with_value(text:current_user.email, classes: %w[data-hj-suppress]) - - row.with_action(text: 'Change email', href: edit_email_user_path, html_attributes: { id: :edit_email_user }) - - your_details.with_row do |row| - - row.with_key { 'Password' } - - row.with_value { t('my_account.password_changed', date: current_user.password_last_changed) } - - row.with_action(text: 'Change password', href: edit_password_user_path, html_attributes: { id: :edit_password_user }) + .govuk-body-small + = m('my_account.name_information') = govuk_summary_list do |other_details| - other_details.with_row do |row| From adaa7027e2c0d0e5cce00d88d09f42d96e4ec1c0 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 14 Nov 2023 14:00:28 +0000 Subject: [PATCH 045/122] create gov one bridging page --- app/controllers/gov_one_controller.rb | 7 ++++++- app/views/gov_one/info.html.slim | 30 +++++++++++++++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/controllers/gov_one_controller.rb b/app/controllers/gov_one_controller.rb index 8c4f49d46..33fc1aafd 100644 --- a/app/controllers/gov_one_controller.rb +++ b/app/controllers/gov_one_controller.rb @@ -1,3 +1,8 @@ class GovOneController < ApplicationController - def info; end + + layout "hero" + + def info + redirect_to my_modules_path if current_user + end end diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index fce694838..a97326df7 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -1,14 +1,26 @@ -= render 'learning/cms_debug' - - content_for :page_title do - = html_title t('gov_one.title') + = html_title t('home.title') + +- content_for :hero do + .govuk-grid-row class='govuk-!-padding-top-9 govuk-!-padding-bottom-0' + .govuk-grid-column-three-quarters + h1.dfe-heading-xl class='govuk-!-margin-bottom-4' + = t('gov-one-info.hero-header') + p.govuk-body-l = t('gov-one-info.hero-body') + .govuk-grid-row - .govuk-grid-column-one-half - = m('gov_one.body') + .govuk-grid-column-three-quarters + . class='govuk-!-margin-bottom-5' + = m('gov-one-info.body') + hr + + = govuk_button_link_to t('gov-one-info.sign-int-button'), login_uri - - if current_user - = govuk_button_link_to t('gov_one.links.sign_out'), logout_uri.to_s - - else - = govuk_button_link_to t('gov_one.links.sign_in'), login_uri.to_s + details.govuk-details class='govuk-!-margin-top-6' data-module='govuk-details' + summary.govuk-details__summary + span.govuk-details__summary-text + | How to access an existing training account + .govuk-details__text + | If you have an existing early years child development training account but you do not yet have a GOV.UK One Login, you must use the same email address for both accounts. This will ensure that any progress you have made through the training is retained. \ No newline at end of file From a15457b3977a5883cfce7d3c76756f1d85e23d15 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 14 Nov 2023 17:08:17 +0000 Subject: [PATCH 046/122] update gov one helper and specs --- .env.example | 4 -- .../users/omniauth_callbacks_controller.rb | 2 - app/helpers/gov_one_helper.rb | 14 ++++- app/models/user.rb | 5 +- app/views/gov_one/info.html.slim | 4 +- config/application.rb | 8 +-- config/credentials/test.yml.enc | 2 +- config/routes.rb | 2 +- docker-compose.dev.yml | 3 +- spec/helpers/gov_one_helper_spec.rb | 34 +++++++----- spec/models/user_spec.rb | 35 ++++++++++++ spec/services/gov_one_auth_service_spec.rb | 55 +++++++++++++------ 12 files changed, 117 insertions(+), 51 deletions(-) diff --git a/.env.example b/.env.example index ae0b13f45..781f4309c 100644 --- a/.env.example +++ b/.env.example @@ -82,10 +82,6 @@ E2E=true # Deployment environment ENVIRONMENT= -# Gov One Authentication -GOV_ONE_LOGIN=true -GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk - # DISPLAY SERVICE_UNAVAILABLE PAGE MAINTENANCE = true \ No newline at end of file diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index bed63b459..817d3281d 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -5,8 +5,6 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # This method is called by Devise after successful Gov One Login authentication # @return [nil] def openid_connect - return error_redirect unless valid_params? || Rails.application.gov_one_login? - session.delete(:gov_one_auth_state) auth_service = GovOneAuthService.new(code: params['code']) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index b9fea0c05..13027decf 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -12,7 +12,7 @@ def login_uri session[:gov_one_auth_state] = params[:state] - gov_one_uri(:login, params) + gov_one_uri(:login, params).to_s end # @return [String] @@ -23,7 +23,17 @@ def logout_uri post_logout_redirect_uri: GovOneAuthService::CALLBACKS[:logout], } - gov_one_uri(:logout, params) + gov_one_uri(:logout, params).to_s + end + + # @return [String] + def login_button + govuk_button_link_to t('gov_one.links.sign_in'), login_uri + end + + # @return [String] + def logout_button + govuk_button_link_to t('gov_one.links.sign_out'), logout_uri end private diff --git a/app/models/user.rb b/app/models/user.rb index 711c392a3..f16fdab98 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,8 +26,9 @@ def self.dashboard_headers # @return [User] def self.find_or_create_from_gov_one(email:, gov_one_id:) if (user = find_by(email: email) || find_by(gov_one_id: gov_one_id)) - user.update!(email: email) - user.update!(gov_one_id: gov_one_id) if user.gov_one_id.nil? + user.update_column(:email, email) + user.update_column(:gov_one_id, gov_one_id) if user.gov_one_id.nil? + user.save! else user = new(email: email, gov_one_id: gov_one_id, confirmed_at: Time.zone.now) user.save!(validate: false) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index fce694838..6969b39f2 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -8,7 +8,7 @@ = m('gov_one.body') - if current_user - = govuk_button_link_to t('gov_one.links.sign_out'), logout_uri.to_s + = logout_button - else - = govuk_button_link_to t('gov_one.links.sign_in'), login_uri.to_s + = login_button \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 8ea687452..182ddd68f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -71,9 +71,9 @@ class Application < Rails::Application config.contentful_environment = ENV.fetch('CONTENTFUL_ENVIRONMENT', credentials.dig(:contentful, :environment)) # Gov one - config.gov_one_base_uri = ENV.fetch('GOV_ONE_BASE_URI', credentials.dig(:gov_one, :base_uri)) - config.gov_one_private_key = ENV.fetch('GOV_ONE_PRIVATE_KEY', credentials.dig(:gov_one, :private_key)) - config.gov_one_client_id = ENV.fetch('GOV_ONE_CLIENT_ID', credentials.dig(:gov_one, :client_id)) + config.gov_one_base_uri = credentials.dig(:gov_one, :base_uri) + config.gov_one_private_key = credentials.dig(:gov_one, :private_key) + config.gov_one_client_id = credentials.dig(:gov_one, :client_id) # @return [Boolean] def live? @@ -115,7 +115,7 @@ def debug? # @return [Boolean] def gov_one_login? - Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', true)] && !live? + Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] || !live? end # @return [Boolean] diff --git a/config/credentials/test.yml.enc b/config/credentials/test.yml.enc index 69becba05..d7eaeabb8 100644 --- a/config/credentials/test.yml.enc +++ b/config/credentials/test.yml.enc @@ -1 +1 @@ -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--ub0s9DBRWGcD5tT1--YSXZBp3jUhqXA6bl51La3w== \ No newline at end of file +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--Qdgcpq2DkjafVGqU--C3fQKZmL1SwRwBJpGQ3IZg== \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 0dc304b67..9de3c4b5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview - get 'gov-one/info', to: 'gov_one#info' if Rails.application.gov_one_login? + get 'gov-one/info', to: 'gov_one#info' get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8e3a4eeed..7287b9819 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,9 +19,8 @@ services: # app config - DATABASE_URL=postgres://postgres:password@db:5432/early_years_foundation_recovery_development - RAILS_ENV=development - - DOMAIN=localhost:3000 # put back to app + - DOMAIN=app:3000 - RAILS_SERVE_STATIC_FILES=true - - GOV_ONE_BASE_URI=https://oidc.integration.account.gov.uk volumes: - .:/srv tty: true diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index 33fe599d3..5ec6f27cd 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -4,28 +4,36 @@ describe '#login_uri' do subject(:login_uri) { helper.login_uri } - it 'returns a URI object with the correct host and path' do - expect(login_uri.host).to eq 'oidc.test.account.gov.uk' - expect(login_uri.path).to eq '/authorize' - end - it 'encodes the authorize endpoint params' do - expect(login_uri.query).to start_with 'response_type=code&scope=email+openid&client_id=' - expect(login_uri.query).to end_with '&redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' + expect(login_uri).to start_with 'https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=' + expect(login_uri).to end_with 'redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' end end describe '#logout_uri' do subject(:logout_uri) { helper.logout_uri } - it 'returns a URI object with the correct host and path' do - expect(logout_uri.host).to eq 'oidc.test.account.gov.uk' - expect(logout_uri.path).to eq '/logout' + it 'encodes the logout endpoint params' do + expect(logout_uri).to start_with 'https://oidc.test.account.gov.uk/logout?id_token_hint&state=' + expect(logout_uri).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' end + end - it 'encodes the logout endpoint params' do - expect(logout_uri.query).to start_with 'id_token_hint&state=' - expect(logout_uri.query).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' + describe '#login_button' do + subject(:login_button) { helper.login_button } + + it 'returns a link to the login uri' do + expect(login_button).to include 'govuk-button' + expect(login_button).to include 'Sign in with Gov One Login' + end + end + + describe '#logout_button' do + subject(:logout_button) { helper.logout_button } + + it 'returns a link to the login uri' do + expect(logout_button).to include 'govuk-button' + expect(logout_button).to include 'Sign out of Gov One Login' end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fd0217a17..220633237 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -206,4 +206,39 @@ end end end + + describe '.find_or_create_from_gov_one' do + let(:email) { 'test@test.com' } + let(:gov_one_id) { '123' } + + context 'when user exists with email but no gov_one_id' do + let!(:user) { create(:user, email: email) } + + it 'updates the user gov_one_id' do + described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) + expect(user.reload.gov_one_id).to eq(gov_one_id) + end + end + + context 'when user exists with gov_one_id' do + let!(:user) { create(:user, gov_one_id: gov_one_id) } + + it 'updates the user email' do + described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) + expect(user.reload.email).to eq(email) + end + end + + context 'when user does not exist' do + let(:email) { 'some_new_email@test.com' } + let(:gov_one_id) { '321' } + + it 'creates a new user' do + described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) + expect(described_class.count).to eq(1) + expect(described_class.first.email).to eq(email) + expect(described_class.first.gov_one_id).to eq(gov_one_id) + end + end + end end diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 6f795b0c4..564ccfa14 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -56,27 +56,46 @@ end describe 'CALLBACKS' do - it 'returns a frozen hash of callbacks urls' do - expect(described_class::CALLBACKS).to eq({ - login: 'http://recovery.app/users/auth/openid_connect/callback', - logout: 'http://recovery.app/users/sign_out', - }) + subject(:callbacks) { described_class::CALLBACKS } - expect(described_class::CALLBACKS).to be_frozen + specify 'callbacks' do + expect(callbacks).to be_frozen + end + + specify 'login' do + expect(callbacks[:login]).to eq 'http://recovery.app/users/auth/openid_connect/callback' + end + + specify 'logout' do + expect(callbacks[:logout]).to eq 'http://recovery.app/users/sign_out' end end describe 'ENDPOINTS' do - it 'returns a frozen hash of endpoints urls' do - expect(described_class::ENDPOINTS).to eq({ - login: 'https://oidc.test.account.gov.uk/authorize', - logout: 'https://oidc.test.account.gov.uk/logout', - token: 'https://oidc.test.account.gov.uk/token', - userinfo: 'https://oidc.test.account.gov.uk/userinfo', - jwks: 'https://oidc.test.account.gov.uk/.well-known/jwks.json', - }) - - expect(described_class::ENDPOINTS).to be_frozen + subject(:endpoints) { described_class::ENDPOINTS } + + specify 'endpoints' do + expect(endpoints).to be_frozen + end + + specify 'login' do + expect(endpoints[:login]).to eq 'https://oidc.test.account.gov.uk/authorize' + end + + specify 'logout' do + expect(endpoints[:logout]).to eq 'https://oidc.test.account.gov.uk/logout' + end + + specify 'token' do + expect(endpoints[:token]).to eq 'https://oidc.test.account.gov.uk/token' + end + + specify 'userinfo' do + expect(endpoints[:userinfo]).to eq 'https://oidc.test.account.gov.uk/userinfo' + end + + specify 'jwks' do + expect(endpoints[:jwks]).to eq 'https://oidc.test.account.gov.uk/.well-known/jwks.json' end end @@ -96,8 +115,8 @@ it 'returns a hash of correct jwt payload' do expect(jwt_payload[:aud]).to eq('https://oidc.test.account.gov.uk/token') - expect(jwt_payload[:iss]).to be_a(String) - expect(jwt_payload[:sub]).to be_a(String) + expect(jwt_payload[:iss]).to eq('some_client_id') + expect(jwt_payload[:sub]).to eq('some_client_id') expect(jwt_payload[:exp]).to be_between(Time.zone.now.to_i + 4 * 60, Time.zone.now.to_i + 6 * 60) expect(jwt_payload[:jti]).to be_a(String) expect(jwt_payload[:iat]).to be_a(Integer) From 8410f8fe8ba0056358c0eb34b1461dab93e41cbf Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 15 Nov 2023 14:21:12 +0000 Subject: [PATCH 047/122] update bridging page --- app/views/gov_one/info.html.slim | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index 265026392..3522640ec 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -17,10 +17,11 @@ = govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri - details.govuk-details class='govuk-!-margin-top-6 govuk-!-margin-bottom-9' data-module='govuk-details' - summary.govuk-details__summary - span.govuk-details__summary-text - | How to access an existing training account - .govuk-details__text - | If you have an existing early years child development training account but you do not yet have a GOV.UK One Login, you must use the same email address for both accounts. This will ensure that any progress you have made through the training is retained. +. class='govuk-!-margin-top-6 govuk-!-margin-bottom-9' + details.govuk-details data-module='govuk-details' + summary.govuk-details__summary + span.govuk-details__summary-text + = t('gov-one-info.details-summary') + .govuk-details__text + = t('gov-one-info.details-text') \ No newline at end of file From b7a8ff54cdbca449431cababc258c929c052327c Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 09:59:12 +0000 Subject: [PATCH 048/122] update homepage --- app/controllers/gov_one_controller.rb | 3 +-- app/views/home/_hero.html.slim | 7 ++---- app/views/home/index.html.slim | 17 ++++++--------- spec/system/gov_one_info_spec.rb | 31 +++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 spec/system/gov_one_info_spec.rb diff --git a/app/controllers/gov_one_controller.rb b/app/controllers/gov_one_controller.rb index 33fc1aafd..de6d437dd 100644 --- a/app/controllers/gov_one_controller.rb +++ b/app/controllers/gov_one_controller.rb @@ -1,6 +1,5 @@ class GovOneController < ApplicationController - - layout "hero" + layout 'hero' def info redirect_to my_modules_path if current_user diff --git a/app/views/home/_hero.html.slim b/app/views/home/_hero.html.slim index 3406bc55d..a148caac1 100644 --- a/app/views/home/_hero.html.slim +++ b/app/views/home/_hero.html.slim @@ -7,15 +7,12 @@ p.govuk-body-l = t('home.hero') - = govuk_button_link_to course_overview_path, class: 'govuk-button--start govuk-!-margin-bottom-4' do + = link_to course_overview_path, class: 'govuk-!-margin-bottom-4 govuk-link--no-visited-state govuk-!-font-weight-bold govuk-body' do - if current_user | Learn more - else - | Learn more and enrol + | Learn more about this training p.govuk-visually-hidden on the course - svg.govuk-button__start-icon xmlns='http://www.w3.org/2000/svg' width='17.5' height='19' viewBox='0 0 33 40' aria-hidden='true' focusable='false' - path fill='currentColor' d='M0 0h13l20 20-20 20H0l20-20z' - .govuk-grid-column-one-third class='govuk-!-text-align-right' = m('home.thumb') \ No newline at end of file diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index ac3501a91..c29377f96 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -11,16 +11,6 @@ .govuk-grid-column-one-half = m('home.about', headings_start_with: 'xl') - - unless current_user - .govuk-grid-column-one-half - .light-grey-box.enrol-box - = m('home.login', headings_start_with: 'xl') - - .govuk-button-group - = govuk_button_link_to 'Sign in', new_user_session_path - .white-space-pre-wrap= ' or ' - = govuk_link_to 'create an account', new_user_registration_path - .prompt.prompt-home .govuk-grid-row .govuk-grid-column-one-quarter @@ -28,3 +18,10 @@ .govuk-grid-column-three-quarters = m('home.prompt', headings_start_with: 'xl') + +- unless current_user + .govuk-button-group + = govuk_button_link_to 'gov-one/info', class: "govuk-button--start" do + | #{t('home.gov-one-button')} + svg.govuk-button__start-icon xmlns='http://www.w3.org/2000/svg' width='17.5' height='19' viewBox='0 0 33 40' aria-hidden='true' focusable='false' + path fill='currentColor' d='M0 0h13l20 20-20 20H0l20-20z' diff --git a/spec/system/gov_one_info_spec.rb b/spec/system/gov_one_info_spec.rb new file mode 100644 index 000000000..9a7073f93 --- /dev/null +++ b/spec/system/gov_one_info_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe 'Gov One Info' do + context 'when the user is not logged in' do + before do + visit '/gov-one/info' + end + + it 'displays the correct content' do + expect(page).to have_css('h1', text: 'How to access this training course') + expect(page).to have_css('p', text: 'This service uses GOV.UK One Login which is managed by the Government Digital Service.') + expect(page).to have_css('p', text: 'You will be asked to sign in to your account, or create a One Login account, in this service') + expect(page).to have_css('a', text: 'Continue to GOV.UK One Login') + end + + it 'has the correct login link' do + link = find_link('Continue to GOV.UK One Login') + expect(link[:href]).to start_with('https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id') + expect(link[:href]).to end_with('&redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback') + end + end + + context 'when the user is logged in' do + include_context 'with user' + + it 'they are redirected to the my modules page' do + visit '/gov-one/info' + expect(page).to have_current_path('/my-modules') + end + end +end From 36ee42d12e27394f9c0a92bce46dc792fed49912 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 10:53:20 +0000 Subject: [PATCH 049/122] update gov one helper and specs --- app/helpers/gov_one_helper.rb | 7 ++----- spec/helpers/gov_one_helper_spec.rb | 12 +++++++----- spec/models/user_spec.rb | 6 +++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 13027decf..c8c30e321 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -7,12 +7,11 @@ def login_uri client_id: Rails.application.config.gov_one_client_id, nonce: SecureRandom.uuid, state: SecureRandom.uuid, - redirect_uri: GovOneAuthService::CALLBACKS[:login], } session[:gov_one_auth_state] = params[:state] - gov_one_uri(:login, params).to_s + "#{gov_one_uri(:login, params)}&redirect_uri=#{GovOneAuthService::CALLBACKS[:login]}" end # @return [String] @@ -20,10 +19,8 @@ def logout_uri params = { id_token_hint: session[:id_token], state: SecureRandom.uuid, - post_logout_redirect_uri: GovOneAuthService::CALLBACKS[:logout], } - - gov_one_uri(:logout, params).to_s + "#{gov_one_uri(:logout, params)}&post_logout_redirect_uri=#{GovOneAuthService::CALLBACKS[:logout]}" end # @return [String] diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index 5ec6f27cd..f9693a7e3 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -5,8 +5,8 @@ subject(:login_uri) { helper.login_uri } it 'encodes the authorize endpoint params' do - expect(login_uri).to start_with 'https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=' - expect(login_uri).to end_with 'redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' + expect(login_uri).to start_with 'https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id&' + expect(login_uri).to end_with 'redirect_uri=http://recovery.app/users/auth/openid_connect/callback' end end @@ -15,25 +15,27 @@ it 'encodes the logout endpoint params' do expect(logout_uri).to start_with 'https://oidc.test.account.gov.uk/logout?id_token_hint&state=' - expect(logout_uri).to end_with '&post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' + expect(logout_uri).to end_with '&post_logout_redirect_uri=http://recovery.app/users/sign_out' end end describe '#login_button' do subject(:login_button) { helper.login_button } - it 'returns a link to the login uri' do + it 'returns a button link to the gov one login uri' do expect(login_button).to include 'govuk-button' expect(login_button).to include 'Sign in with Gov One Login' + expect(login_button).to include 'href="https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id&' end end describe '#logout_button' do subject(:logout_button) { helper.logout_button } - it 'returns a link to the login uri' do + it 'returns a button link to the gov one logout uri' do expect(logout_button).to include 'govuk-button' expect(logout_button).to include 'Sign out of Gov One Login' + expect(logout_button).to include 'href="https://oidc.test.account.gov.uk/logout?id_token_hint&state=' end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 220633237..1c1d0afa7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -211,7 +211,7 @@ let(:email) { 'test@test.com' } let(:gov_one_id) { '123' } - context 'when user exists with email but no gov_one_id' do + context 'with an existing user having an email but no gov_one_id' do let!(:user) { create(:user, email: email) } it 'updates the user gov_one_id' do @@ -220,7 +220,7 @@ end end - context 'when user exists with gov_one_id' do + context 'with an existing user having a gov_one_id' do let!(:user) { create(:user, gov_one_id: gov_one_id) } it 'updates the user email' do @@ -229,7 +229,7 @@ end end - context 'when user does not exist' do + context 'without an existing user' do let(:email) { 'some_new_email@test.com' } let(:gov_one_id) { '321' } From e35f97e6ccca36cac2ce5033a44b9c68ec09acb0 Mon Sep 17 00:00:00 2001 From: jack-coggin <119428483+jack-coggin@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:58:18 +0000 Subject: [PATCH 050/122] Remove .env.example whitespace Remove whitespace from .env.example --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index 311b11fcd..fac18a28a 100644 --- a/.env.example +++ b/.env.example @@ -85,4 +85,3 @@ E2E=true # Deployment environment ENVIRONMENT= - From 2cf70646c5c7876e61ad8e60ec728b1eb33a86b9 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 11:23:53 +0000 Subject: [PATCH 051/122] update application_helper links, gov_one_helper spec and revert changes to account page --- app/helpers/application_helper.rb | 8 ++++---- app/views/user/show.html.slim | 10 ++++++++-- spec/helpers/gov_one_helper_spec.rb | 12 +----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d7b80bbf6..cdb815c45 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,14 +11,14 @@ def navigation header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) if user_signed_in? header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) - header.with_action_link(text: 'Sign out', href: destroy_user_session_path, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) + header.with_action_link(text: 'Sign out', href: logout_uri, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? header.with_navigation_item(text: 'My account', href: user_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) - header.with_navigation_item(text: 'Sign out', href: destroy_user_session_path, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) + header.with_navigation_item(text: 'Sign out', href: logout_uri, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) else - header.with_action_link(text: 'Sign in', href: new_user_session_path, options: { inverse: true }) - header.with_navigation_item(text: 'Sign in', href: new_user_session_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) + header.with_action_link(text: 'Sign in', href: 'gov-one/info', options: { inverse: true }) + header.with_navigation_item(text: 'Sign in', href: 'gov-one/info', classes: %w[dfe-header__navigation-item dfe-header-f-mob]) end end end diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index ab99e56f1..1cf9f3e03 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -14,8 +14,14 @@ - row.with_key { 'Name' } - row.with_value(text: current_user.name, classes: %w[data-hj-suppress]) - row.with_action(text: 'Change name', href: edit_registration_name_path, html_attributes: { id: :edit_name_registration }) - .govuk-body-small - = m('my_account.name_information') + - your_details.with_row do |row| + - row.with_key { 'Email' } + - row.with_value(text:current_user.email, classes: %w[data-hj-suppress]) + - row.with_action(text: 'Change email', href: edit_email_user_path, html_attributes: { id: :edit_email_user }) + - your_details.with_row do |row| + - row.with_key { 'Password' } + - row.with_value { t('my_account.password_changed', date: current_user.password_last_changed) } + - row.with_action(text: 'Change password', href: edit_password_user_path, html_attributes: { id: :edit_password_user }) = govuk_summary_list do |other_details| - other_details.with_row do |row| diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index f9693a7e3..829e6f149 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -24,18 +24,8 @@ it 'returns a button link to the gov one login uri' do expect(login_button).to include 'govuk-button' - expect(login_button).to include 'Sign in with Gov One Login' + expect(login_button).to include 'Continue to GOV.UK One Login' expect(login_button).to include 'href="https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id&' end end - - describe '#logout_button' do - subject(:logout_button) { helper.logout_button } - - it 'returns a button link to the gov one logout uri' do - expect(logout_button).to include 'govuk-button' - expect(logout_button).to include 'Sign out of Gov One Login' - expect(logout_button).to include 'href="https://oidc.test.account.gov.uk/logout?id_token_hint&state=' - end - end end From 784f11626c3392428136e15c0b7bd2f89b095f60 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 11:58:25 +0000 Subject: [PATCH 052/122] skip whats_new_page_spec and update locales --- config/locales/en.yml | 2 -- spec/system/whats_new_page_spec.rb | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index fdff220a9..9d2d87198 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -508,8 +508,6 @@ en: home: title: Home page hero: | - # %{service_name} - This free, online training provides an overview of child development and gives practical advice for supporting the development of children in your early years setting. thumb: |  diff --git a/spec/system/whats_new_page_spec.rb b/spec/system/whats_new_page_spec.rb index 7fc194638..1a86631ff 100644 --- a/spec/system/whats_new_page_spec.rb +++ b/spec/system/whats_new_page_spec.rb @@ -20,18 +20,20 @@ end describe 'with subsequent logins' do - before do - click_on 'sign-out-desktop' + skip 'wip' do + before do + click_on 'sign-out-desktop' - visit '/users/sign-in' - fill_in 'Email address', with: user.email - fill_in 'Password', with: user.password - click_button 'Sign in' - end + visit '/users/sign-in' + fill_in 'Email address', with: user.email + fill_in 'Password', with: user.password + click_button 'Sign in' + end - it 'does not appear' do - expect(page).not_to have_current_path '/whats-new' - expect(page).to have_current_path '/my-modules' + it 'does not appear' do + expect(page).not_to have_current_path '/whats-new' + expect(page).to have_current_path '/my-modules' + end end end end From 4451a126953fab4d0c7705a8d39dfb2817b7a627 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 14:37:04 +0000 Subject: [PATCH 053/122] use content from cms --- app/helpers/gov_one_helper.rb | 4 ++-- app/views/gov_one/info.html.slim | 2 +- config/locales/en.yml | 11 ----------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index c8c30e321..789ed2aea 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -25,12 +25,12 @@ def logout_uri # @return [String] def login_button - govuk_button_link_to t('gov_one.links.sign_in'), login_uri + govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri end # @return [String] def logout_button - govuk_button_link_to t('gov_one.links.sign_out'), logout_uri + govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri end private diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index 6969b39f2..74040bf19 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -5,7 +5,7 @@ .govuk-grid-row .govuk-grid-column-one-half - = m('gov_one.body') + = m('gov-one-info.hero-body') - if current_user = logout_button diff --git a/config/locales/en.yml b/config/locales/en.yml index fdff220a9..6a261b5d7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -574,17 +574,6 @@ en: %{criteria} - # /gov-one/info - gov_one: - title: Gov One Login - body: | - # Gov One Login - - This is some information about Gov One Login... - links: - sign_in: Sign in with Gov One Login - sign_out: Sign out of Gov One Login - # /settings/cookie-policy cookie_policy: title: Cookie policy From dadd4b8cc6214b37317c5e857423627a5c42651a Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 14:50:26 +0000 Subject: [PATCH 054/122] update helper spec --- spec/helpers/gov_one_helper_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index f9693a7e3..10b923853 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -24,7 +24,7 @@ it 'returns a button link to the gov one login uri' do expect(login_button).to include 'govuk-button' - expect(login_button).to include 'Sign in with Gov One Login' + expect(login_button).to include 'Continue to GOV.UK One Login' expect(login_button).to include 'href="https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id&' end end From 7b776bec7a8b892d5ef52fb27d56217c07a179bc Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 15:22:36 +0000 Subject: [PATCH 055/122] add route alias for bridging page --- app/helpers/application_helper.rb | 4 ++-- app/views/home/index.html.slim | 2 +- config/routes.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cdb815c45..623e5c338 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -17,8 +17,8 @@ def navigation header.with_navigation_item(text: 'My account', href: user_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) header.with_navigation_item(text: 'Sign out', href: logout_uri, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) else - header.with_action_link(text: 'Sign in', href: 'gov-one/info', options: { inverse: true }) - header.with_navigation_item(text: 'Sign in', href: 'gov-one/info', classes: %w[dfe-header__navigation-item dfe-header-f-mob]) + header.with_action_link(text: 'Sign in', href: gov_one_info_path, options: { inverse: true }) + header.with_navigation_item(text: 'Sign in', href: gov_one_info_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) end end end diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index c29377f96..771ecd25b 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -21,7 +21,7 @@ - unless current_user .govuk-button-group - = govuk_button_link_to 'gov-one/info', class: "govuk-button--start" do + = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do | #{t('home.gov-one-button')} svg.govuk-button__start-icon xmlns='http://www.w3.org/2000/svg' width='17.5' height='19' viewBox='0 0 33 40' aria-hidden='true' focusable='false' path fill='currentColor' d='M0 0h13l20 20-20 20H0l20-20z' diff --git a/config/routes.rb b/config/routes.rb index 9de3c4b5c..a93482981 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview - get 'gov-one/info', to: 'gov_one#info' + get 'gov-one/info', to: 'gov_one#info', as: :gov_one_info get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all From 9765caf5b0847ebede660f73ab702040cd260c75 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 15:26:18 +0000 Subject: [PATCH 056/122] extract homepage cta to partial --- app/views/home/_cta.html.slim | 5 +++++ app/views/home/index.html.slim | 6 +----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 app/views/home/_cta.html.slim diff --git a/app/views/home/_cta.html.slim b/app/views/home/_cta.html.slim new file mode 100644 index 000000000..1c114a821 --- /dev/null +++ b/app/views/home/_cta.html.slim @@ -0,0 +1,5 @@ + .govuk-button-group + = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do + | #{t('home.gov-one-button')} + svg.govuk-button__start-icon xmlns='http://www.w3.org/2000/svg' width='17.5' height='19' viewBox='0 0 33 40' aria-hidden='true' focusable='false' + path fill='currentColor' d='M0 0h13l20 20-20 20H0l20-20z' \ No newline at end of file diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 771ecd25b..5c4543f0a 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -20,8 +20,4 @@ = m('home.prompt', headings_start_with: 'xl') - unless current_user - .govuk-button-group - = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do - | #{t('home.gov-one-button')} - svg.govuk-button__start-icon xmlns='http://www.w3.org/2000/svg' width='17.5' height='19' viewBox='0 0 33 40' aria-hidden='true' focusable='false' - path fill='currentColor' d='M0 0h13l20 20-20 20H0l20-20z' + = render 'cta' From 6d74e30cc2059d4f16fd6d78c39f8de752c215f2 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 16 Nov 2023 16:08:59 +0000 Subject: [PATCH 057/122] remove unnecessary route alias --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index a93482981..9de3c4b5c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview - get 'gov-one/info', to: 'gov_one#info', as: :gov_one_info + get 'gov-one/info', to: 'gov_one#info' get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all From dd05c11a84be85350dd0a99e072556785797a7f5 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Thu, 16 Nov 2023 16:46:29 +0000 Subject: [PATCH 058/122] Retest local dev with encoded redirect params --- app/helpers/gov_one_helper.rb | 17 ++++++++++------- docker-compose.dev.yml | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 789ed2aea..5791a5c14 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -1,36 +1,39 @@ module GovOneHelper - # @return [String] + # @return [URI] def login_uri params = { + redirect_uri: GovOneAuthService::CALLBACKS[:login], + client_id: Rails.application.config.gov_one_client_id, response_type: 'code', scope: 'email openid', - client_id: Rails.application.config.gov_one_client_id, nonce: SecureRandom.uuid, state: SecureRandom.uuid, } session[:gov_one_auth_state] = params[:state] - "#{gov_one_uri(:login, params)}&redirect_uri=#{GovOneAuthService::CALLBACKS[:login]}" + gov_one_uri(:login, params) end - # @return [String] + # @return [URI] def logout_uri params = { + post_logout_redirect_uri: GovOneAuthService::CALLBACKS[:logout], id_token_hint: session[:id_token], state: SecureRandom.uuid, } - "#{gov_one_uri(:logout, params)}&post_logout_redirect_uri=#{GovOneAuthService::CALLBACKS[:logout]}" + + gov_one_uri(:logout, params) end # @return [String] def login_button - govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri + govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri.to_s end # @return [String] def logout_button - govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri + govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri.to_s end private diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7287b9819..11001e395 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,7 +19,8 @@ services: # app config - DATABASE_URL=postgres://postgres:password@db:5432/early_years_foundation_recovery_development - RAILS_ENV=development - - DOMAIN=app:3000 + # - DOMAIN=app:3000 + - DOMAIN=localhost:3000 - RAILS_SERVE_STATIC_FILES=true volumes: - .:/srv From 59bc7410ed9a4a8baa81897c31e211a9ed838c62 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Fri, 17 Nov 2023 08:55:47 +0000 Subject: [PATCH 059/122] Highlight duplication in the service spec with a little refactor more to do??? --- app/services/gov_one_auth_service.rb | 7 -- spec/services/gov_one_auth_service_spec.rb | 85 ++++++++++------------ 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 7f1d2809a..786ec439c 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -59,13 +59,6 @@ def response(request, http) http.request(request) end - # @param request [Net::HTTP::Get, Net::HTTP::Post] - # @return [Hash] - def handle_request(request) - response = http.request(request) - JSON.parse(response.body) - end - # @param token [String] # @return [Array<Hash>] def decode_id_token(token) diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 564ccfa14..d6adfbf89 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -8,25 +8,24 @@ before do allow(auth_service).to receive(:response).and_return(mock_response) + allow(mock_response).to receive(:body).and_return(payload.to_json) end describe '#tokens' do let(:result) { auth_service.tokens } - context 'when the request is successful' do + context 'when successful' do it 'returns a hash of tokens' do - allow(mock_response).to receive(:body).and_return(payload.to_json) - expect(result).to eq(payload) expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Post), an_instance_of(Net::HTTP)) end end - context 'when the request is unsuccessful' do - it 'returns an empty hash' do - allow(mock_response).to receive(:body).and_return({}.to_json) + context 'when unsuccessful' do + let(:payload) { {} } - expect(result).to eq({}) + it 'returns an empty hash' do + expect(result).to eq(payload) expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Post), an_instance_of(Net::HTTP)) end end @@ -36,31 +35,49 @@ let(:access_token) { 'mock_access_token' } let(:result) { auth_service.user_info(access_token) } - context 'when the request is successful' do + context 'when successful' do it 'returns a hash of user info' do - allow(mock_response).to receive(:body).and_return(payload.to_json) - expect(result).to eq(payload) expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Get), an_instance_of(Net::HTTP)) end end - context 'when the request is unsuccessful' do - it 'returns an empty hash' do - allow(mock_response).to receive(:body).and_return({}.to_json) + context 'when unsuccessful' do + let(:payload) { {} } - expect(result).to eq({}) + it 'returns an empty hash' do + expect(result).to eq(payload) expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Get), an_instance_of(Net::HTTP)) end end end - describe 'CALLBACKS' do - subject(:callbacks) { described_class::CALLBACKS } + describe '#token_body' do + let(:token_body) { auth_service.send(:token_body) } + + it 'returns a hash of correct token body' do + expect(token_body[:grant_type]).to eq('authorization_code') + expect(token_body[:code]).to eq(code) + expect(token_body[:redirect_uri]).to end_with('/users/auth/openid_connect/callback') + expect(token_body[:client_assertion_type]).to eq('urn:ietf:params:oauth:client-assertion-type:jwt-bearer') + end + end + + describe '#jwt_payload' do + let(:jwt_payload) { auth_service.send(:jwt_payload) } - specify 'callbacks' do - expect(callbacks).to be_frozen + it 'returns a hash of correct jwt payload' do + expect(jwt_payload[:aud]).to eq 'https://oidc.test.account.gov.uk/token' + expect(jwt_payload[:iss]).to eq 'some_client_id' + expect(jwt_payload[:sub]).to eq 'some_client_id' + expect(jwt_payload[:exp]).to be_between(Time.zone.now.to_i + 4 * 60, Time.zone.now.to_i + 6 * 60) + expect(jwt_payload[:jti]).to be_a String + expect(jwt_payload[:iat]).to be_a Integer end + end + + describe 'Internal callbacks' do + subject(:callbacks) { described_class::CALLBACKS } specify 'login' do expect(callbacks[:login]).to eq 'http://recovery.app/users/auth/openid_connect/callback' @@ -71,13 +88,11 @@ end end - describe 'ENDPOINTS' do + describe 'OIDC endpoints' do subject(:endpoints) { described_class::ENDPOINTS } - specify 'endpoints' do - expect(endpoints).to be_frozen - end - + # opportunity to add more detail and use specs for documentation on exactly what each endpoint + # is for in the description text specify 'login' do expect(endpoints[:login]).to eq 'https://oidc.test.account.gov.uk/authorize' end @@ -98,28 +113,4 @@ expect(endpoints[:jwks]).to eq 'https://oidc.test.account.gov.uk/.well-known/jwks.json' end end - - describe '#token_body' do - let(:token_body) { auth_service.send(:token_body) } - - it 'returns a hash of correct token body' do - expect(token_body[:grant_type]).to eq('authorization_code') - expect(token_body[:code]).to eq(code) - expect(token_body[:redirect_uri]).to end_with('/users/auth/openid_connect/callback') - expect(token_body[:client_assertion_type]).to eq('urn:ietf:params:oauth:client-assertion-type:jwt-bearer') - end - end - - describe '#jwt_payload' do - let(:jwt_payload) { auth_service.send(:jwt_payload) } - - it 'returns a hash of correct jwt payload' do - expect(jwt_payload[:aud]).to eq('https://oidc.test.account.gov.uk/token') - expect(jwt_payload[:iss]).to eq('some_client_id') - expect(jwt_payload[:sub]).to eq('some_client_id') - expect(jwt_payload[:exp]).to be_between(Time.zone.now.to_i + 4 * 60, Time.zone.now.to_i + 6 * 60) - expect(jwt_payload[:jti]).to be_a(String) - expect(jwt_payload[:iat]).to be_a(Integer) - end - end end From bda11baf0025a9f573346faeee5b0d3ab3d25191 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Fri, 17 Nov 2023 10:24:58 +0000 Subject: [PATCH 060/122] Users::OmniauthCallbacksController with an existing gov-one user redirects to /my-modules with an existing non-gov-one user redirects to /my-modules updates the account with a new user creates an account redirects to complete registration --- .../omniauth_callbacks_controller_spec.rb | 100 ++++++++++-------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index c28512af5..0cf73f971 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -1,58 +1,66 @@ require 'rails_helper' RSpec.describe Users::OmniauthCallbacksController, type: :controller do + let(:auth_service) { instance_double(GovOneAuthService) } + let(:access_token) { 'mock_access_token' } + let(:id_token) { 'mock_id_token' } + let(:email) { 'test@example.com' } + let(:params) do + { 'code' => 'mock_code', 'state' => 'mock_state' } + end + before do request.env['devise.mapping'] = Devise.mappings[:user] + session[:gov_one_auth_state] = 'mock_state' + + allow(GovOneAuthService).to receive(:new).and_return(auth_service) + allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) + allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) + allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') + allow(auth_service).to receive(:decode_id_token).and_return([{ 'sub' => 'mock_sub' }]) + end + + context 'with a new user' do + before do + get :openid_connect, params: params + end + + it 'creates an account' do + expect(User.find_by(email: email)).to be_truthy + expect(User.find_by(gov_one_id: 'mock_sub')).to be_truthy + end + + it 'redirects to complete registration' do + expect(session[:id_token]).to eq id_token + expect(response).to redirect_to edit_registration_name_path + end end - describe '#openid_connect' do - let(:auth_service) { instance_double(GovOneAuthService) } - let(:access_token) { 'mock_access_token' } - let(:id_token) { 'mock_id_token' } - let(:email) { 'test@example.com' } + context 'with an existing non-gov-one user' do + before do + create :user, :registered, email: email + get :openid_connect, params: params + end + + it 'updates the account' do + expect(User.find_by(gov_one_id: 'mock_sub')).to be_truthy + end + + it 'redirects to /my-modules' do + expect(session[:id_token]).to eq id_token + expect(response).to redirect_to my_modules_path + end + end + context 'with an existing gov-one user' do before do - allow(GovOneAuthService).to receive(:new).and_return(auth_service) - allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) - allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) - allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') - allow(auth_service).to receive(:decode_id_token).and_return([{ 'sub' => 'mock_sub' }]) - session[:gov_one_auth_state] = 'mock_state' - end - - context 'when a User does not exist' do - it 'creates the user with the email and id_token' do - get :openid_connect, params: { 'code' => 'mock_code', 'state' => 'mock_state' } - expect(User.find_by(email: email)).to be_truthy - expect(User.find_by(gov_one_id: 'mock_sub')).to be_truthy - expect(response).to redirect_to(edit_registration_name_path) - expect(session[:id_token]).to eq(id_token) - end - end - - context 'when a User email exists' do - before do - create :user, :registered, email: email - end - - it 'updates the user id_token and signs them in' do - get :openid_connect, params: { 'code' => 'mock_code', 'state' => 'mock_state' } - expect(User.find_by(gov_one_id: 'mock_sub')).to be_truthy - expect(response).to redirect_to(my_modules_path) - expect(session[:id_token]).to eq(id_token) - end - end - - context 'when a User id_token exists' do - before do - create :user, :registered, gov_one_id: 'mock_sub' - end - - it 'signs the user in' do - get :openid_connect, params: { 'code' => 'mock_code', 'state' => 'mock_state' } - expect(response).to redirect_to(my_modules_path) - expect(session[:id_token]).to eq(id_token) - end + create :user, :registered, gov_one_id: 'mock_sub' + get :openid_connect, params: params + end + + it 'redirects to /my-modules' do + expect(session[:id_token]).to eq id_token + expect(response).to redirect_to my_modules_path end end end From 5c6a727fa6636ac36e4727e95ef4f3470452f9c1 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 10:39:48 +0000 Subject: [PATCH 061/122] update helper and service specs --- config/application.rb | 5 ----- spec/helpers/gov_one_helper_spec.rb | 20 ++++++++++++++------ spec/models/user_spec.rb | 21 ++++++++++++++------- spec/services/gov_one_auth_service_spec.rb | 12 +++++------- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/config/application.rb b/config/application.rb index 9979b9a54..42c3e1478 100644 --- a/config/application.rb +++ b/config/application.rb @@ -96,11 +96,6 @@ def debug? Types::Params::Bool[ENV.fetch('DEBUG', false)] end - # @return [Boolean] - def gov_one_login? - Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] || !live? - end - # @return [Boolean] def maintenance? Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] diff --git a/spec/helpers/gov_one_helper_spec.rb b/spec/helpers/gov_one_helper_spec.rb index 10b923853..07ce7b8c5 100644 --- a/spec/helpers/gov_one_helper_spec.rb +++ b/spec/helpers/gov_one_helper_spec.rb @@ -5,8 +5,13 @@ subject(:login_uri) { helper.login_uri } it 'encodes the authorize endpoint params' do - expect(login_uri).to start_with 'https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id&' - expect(login_uri).to end_with 'redirect_uri=http://recovery.app/users/auth/openid_connect/callback' + expect(login_uri.host).to eq 'oidc.test.account.gov.uk' + expect(login_uri.path).to eq '/authorize' + expect(login_uri.query).to include 'redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback' + expect(login_uri.query).to include 'client_id=some_client_id' + expect(login_uri.query).to include 'response_type=code' + expect(login_uri.query).to include 'scope=email+openid' + expect(login_uri.query).to include 'nonce=' end end @@ -14,8 +19,11 @@ subject(:logout_uri) { helper.logout_uri } it 'encodes the logout endpoint params' do - expect(logout_uri).to start_with 'https://oidc.test.account.gov.uk/logout?id_token_hint&state=' - expect(logout_uri).to end_with '&post_logout_redirect_uri=http://recovery.app/users/sign_out' + expect(logout_uri.host).to eq 'oidc.test.account.gov.uk' + expect(logout_uri.path).to eq '/logout' + expect(logout_uri.query).to include 'post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out' + expect(logout_uri.query).to include 'id_token_hint' + expect(logout_uri.query).to include 'state=' end end @@ -25,7 +33,7 @@ it 'returns a button link to the gov one login uri' do expect(login_button).to include 'govuk-button' expect(login_button).to include 'Continue to GOV.UK One Login' - expect(login_button).to include 'href="https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id&' + expect(login_button).to include 'href="https://oidc.test.account.gov.uk/authorize?redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback&client_id=some_client_id&response_type=code&scope=email+openid&nonce=' end end @@ -35,7 +43,7 @@ it 'returns a button link to the gov one logout uri' do expect(logout_button).to include 'govuk-button' expect(logout_button).to include 'Sign out of Gov One Login' - expect(logout_button).to include 'href="https://oidc.test.account.gov.uk/logout?id_token_hint&state=' + expect(logout_button).to include 'href="https://oidc.test.account.gov.uk/logout?post_logout_redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fsign_out&id_token_hint&state=' end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1c1d0afa7..c1b68455c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -208,33 +208,40 @@ end describe '.find_or_create_from_gov_one' do + subject(:find_or_create) { described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) } + let(:email) { 'test@test.com' } let(:gov_one_id) { '123' } + let(:user) { create(:user) } context 'with an existing user having an email but no gov_one_id' do - let!(:user) { create(:user, email: email) } + before do + user.update_column(:email, email) + find_or_create + end it 'updates the user gov_one_id' do - described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) expect(user.reload.gov_one_id).to eq(gov_one_id) end end context 'with an existing user having a gov_one_id' do - let!(:user) { create(:user, gov_one_id: gov_one_id) } + before do + user.update_column(:gov_one_id, gov_one_id) + find_or_create + end it 'updates the user email' do - described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) expect(user.reload.email).to eq(email) end end context 'without an existing user' do - let(:email) { 'some_new_email@test.com' } - let(:gov_one_id) { '321' } + before do + find_or_create + end it 'creates a new user' do - described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) expect(described_class.count).to eq(1) expect(described_class.first.email).to eq(email) expect(described_class.first.gov_one_id).to eq(gov_one_id) diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index d6adfbf89..66b65187b 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -91,25 +91,23 @@ describe 'OIDC endpoints' do subject(:endpoints) { described_class::ENDPOINTS } - # opportunity to add more detail and use specs for documentation on exactly what each endpoint - # is for in the description text - specify 'login' do + specify 'login endpoint for starting gov one user session and redirecting back to service' do expect(endpoints[:login]).to eq 'https://oidc.test.account.gov.uk/authorize' end - specify 'logout' do + specify 'logout endpoint for ending gov one user session and redirecting back to service' do expect(endpoints[:logout]).to eq 'https://oidc.test.account.gov.uk/logout' end - specify 'token' do + specify 'token endpoint for retrieving user access token and id token' do expect(endpoints[:token]).to eq 'https://oidc.test.account.gov.uk/token' end - specify 'userinfo' do + specify 'userinfo endpoint for retrieving user email' do expect(endpoints[:userinfo]).to eq 'https://oidc.test.account.gov.uk/userinfo' end - specify 'jwks' do + specify 'jwks endpoint for retrieving public key for verifying user id token' do expect(endpoints[:jwks]).to eq 'https://oidc.test.account.gov.uk/.well-known/jwks.json' end end From 6729f63f4c8de89726ff60a181353f10b6f407ed Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 10:57:27 +0000 Subject: [PATCH 062/122] update locales --- app/helpers/gov_one_helper.rb | 4 ++-- app/views/gov_one/info.html.slim | 4 ++-- config/locales/en.yml | 9 +++++++-- spec/lib/seed_snippets_spec.rb | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 5791a5c14..9369fbbef 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -28,12 +28,12 @@ def logout_uri # @return [String] def login_button - govuk_button_link_to t('gov-one-info.sign-in-button'), login_uri.to_s + govuk_button_link_to t('gov_one_info.sign_in_button'), login_uri.to_s end # @return [String] def logout_button - govuk_button_link_to t('gov-one-info.sign-out-button'), logout_uri.to_s + govuk_button_link_to t('gov_one_info.sign_out_button'), logout_uri.to_s end private diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index 74040bf19..4fd836421 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -1,11 +1,11 @@ = render 'learning/cms_debug' - content_for :page_title do - = html_title t('gov_one.title') + = html_title t('gov_one_info.title') .govuk-grid-row .govuk-grid-column-one-half - = m('gov-one-info.hero-body') + = m('gov_one_info.body') - if current_user = logout_button diff --git a/config/locales/en.yml b/config/locales/en.yml index 6a261b5d7..78cbdf319 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -508,8 +508,6 @@ en: home: title: Home page hero: | - # %{service_name} - This free, online training provides an overview of child development and gives practical advice for supporting the development of children in your early years setting. thumb: |  @@ -574,6 +572,13 @@ en: %{criteria} + # /gov-one/info + gov_one_info: + title: Gov One Info + body: This is some information about Gov One Login. + sign_in_button: Continue to GOV.UK One Login + sign_out_button: Sign out of Gov One Login + # /settings/cookie-policy cookie_policy: title: Cookie policy diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 75a4ac39a..5041dca24 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 187 + expect(locales.count).to be 191 end it 'dot separated key -> Page::Resource#name' do From 703d6424379d794c680bed553e60871d55126386 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 11:28:47 +0000 Subject: [PATCH 063/122] update locales --- config/locales/en.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 78cbdf319..ec06dafcc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -508,6 +508,8 @@ en: home: title: Home page hero: | + # %{service_name} + This free, online training provides an overview of child development and gives practical advice for supporting the development of children in your early years setting. thumb: |  From e17b30e8566cfc410aa62ca210d6ae6a1248af15 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 11:29:11 +0000 Subject: [PATCH 064/122] update locales --- config/locales/en.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/locales/en.yml b/config/locales/en.yml index 65fd41cef..cce7aa516 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -508,6 +508,8 @@ en: home: title: Home page hero: | + # %{service_name} + This free, online training provides an overview of child development and gives practical advice for supporting the development of children in your early years setting. thumb: |  From 391c35b1b0249fba495470b65ff39742e10bde35 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 11:59:38 +0000 Subject: [PATCH 065/122] convert sso auth uri to string in navbar --- app/helpers/application_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 623e5c338..1d4c66edb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,11 +11,11 @@ def navigation header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) if user_signed_in? header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) - header.with_action_link(text: 'Sign out', href: logout_uri, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) + header.with_action_link(text: 'Sign out', href: logout_uri.to_s, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? header.with_navigation_item(text: 'My account', href: user_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) - header.with_navigation_item(text: 'Sign out', href: logout_uri, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) + header.with_navigation_item(text: 'Sign out', href: logout_uri.to_s, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) else header.with_action_link(text: 'Sign in', href: gov_one_info_path, options: { inverse: true }) header.with_navigation_item(text: 'Sign in', href: gov_one_info_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) From 2b4f1a5e6abc241f520d18255d829352c3766074 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 12:08:51 +0000 Subject: [PATCH 066/122] update system spec --- spec/system/front_page_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/system/front_page_spec.rb b/spec/system/front_page_spec.rb index b72275cc3..9216e8620 100644 --- a/spec/system/front_page_spec.rb +++ b/spec/system/front_page_spec.rb @@ -7,16 +7,16 @@ it 'logged in content' do visit '/' expect(page).to have_text 'Learn more' - expect(page).not_to have_text 'Learn more and enrol' - expect(page).not_to have_text 'Sign in to continue learning' + expect(page).not_to have_text 'Learn more about this training' + expect(page).not_to have_text 'Start your training now' end end context 'with an unauthenticated visitor' do it 'log in content' do visit '/' - expect(page).to have_text('Learn more and enrol') - .and have_text('Sign in to continue learning') + expect(page).to have_text('Learn more about this training') + .and have_text('Start your training now') end end end From 208c212a391d97ba2acb367211145242f2b87b24 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 17 Nov 2023 12:36:32 +0000 Subject: [PATCH 067/122] update gov one info system spec --- spec/system/gov_one_info_spec.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/system/gov_one_info_spec.rb b/spec/system/gov_one_info_spec.rb index 9a7073f93..bb252a4b7 100644 --- a/spec/system/gov_one_info_spec.rb +++ b/spec/system/gov_one_info_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe 'Gov One Info' do - context 'when the user is not logged in' do + context 'with an unauthenticated visitor' do before do visit '/gov-one/info' end @@ -15,15 +15,14 @@ it 'has the correct login link' do link = find_link('Continue to GOV.UK One Login') - expect(link[:href]).to start_with('https://oidc.test.account.gov.uk/authorize?response_type=code&scope=email+openid&client_id=some_client_id') - expect(link[:href]).to end_with('&redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback') + expect(link[:href]).to start_with('https://oidc.test.account.gov.uk/authorize?redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback&client_id=some_client_id&response_type=code&scope=email+openid&nonce=') end end - context 'when the user is logged in' do + context 'with an authenticated user' do include_context 'with user' - it 'they are redirected to the my modules page' do + it 'redirects to the my modules page' do visit '/gov-one/info' expect(page).to have_current_path('/my-modules') end From 3b7ba6a7d3cbae5a3158c4a5ed1553e4f7086c90 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 20 Nov 2023 10:07:11 +0000 Subject: [PATCH 068/122] styling tweaks --- app/views/gov_one/info.html.slim | 21 +++++++++++---------- app/views/home/index.html.slim | 9 +++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index a6e57f1d4..8e43b575e 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -2,7 +2,7 @@ = html_title t('home.title') - content_for :hero do - .govuk-grid-row class='govuk-!-padding-top-9 govuk-!-padding-bottom-0' + .govuk-grid-row class='govuk-!-padding-top-9' .govuk-grid-column-three-quarters h1.dfe-heading-xl class='govuk-!-margin-bottom-4' = t('gov_one_info.hero_header') @@ -10,18 +10,19 @@ .govuk-grid-row - .govuk-grid-column-three-quarters + .govuk-grid-column-full . class='govuk-!-margin-bottom-5' = m('gov_one_info.body') hr = login_button -. class='govuk-!-margin-top-6 govuk-!-margin-bottom-9' - details.govuk-details data-module='govuk-details' - summary.govuk-details__summary - span.govuk-details__summary-text - = t('gov_one_info.details_summary') - .govuk-details__text - = t('gov_one_info.details_text') - \ No newline at end of file +.govuk-grid-row + .govuk-grid-column-full class='govuk-!-margin-top-4 govuk-!-margin-bottom-9' + details.govuk-details data-module='govuk-details' + summary.govuk-details__summary + span.govuk-details__summary-text + = t('gov_one_info.details_summary') + .govuk-details__text + = t('gov_one_info.details_text') + \ No newline at end of file diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 8e805cf00..3cc7f73e2 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -20,7 +20,8 @@ = m('home.prompt', headings_start_with: 'xl') - unless current_user - .govuk-button-group - = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do - | #{t('home.gov_one_button')} - = render 'chevron' + .govuk-grid-row class="govuk-!-margin-top-9" + .govuk-grid-column-full + = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do + | #{t('home.gov_one_button')} + = render 'chevron' From b556ac3493e853fd89a7adb0e1ba83a10e5608ca Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 20 Nov 2023 10:39:53 +0000 Subject: [PATCH 069/122] change div width --- app/views/gov_one/info.html.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim index 8e43b575e..f50a52ce6 100644 --- a/app/views/gov_one/info.html.slim +++ b/app/views/gov_one/info.html.slim @@ -18,7 +18,7 @@ = login_button .govuk-grid-row - .govuk-grid-column-full class='govuk-!-margin-top-4 govuk-!-margin-bottom-9' + .govuk-grid-column-three-quarters class='govuk-!-margin-top-4 govuk-!-margin-bottom-9' details.govuk-details data-module='govuk-details' summary.govuk-details__summary span.govuk-details__summary-text From 2d46c4f9466896c6d328267ddfe8a5bfd0949dff Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 20 Nov 2023 15:50:22 +0000 Subject: [PATCH 070/122] update for onboarding tech checklist --- .../users/omniauth_callbacks_controller.rb | 11 +++++---- app/helpers/gov_one_helper.rb | 3 ++- app/services/gov_one_auth_service.rb | 24 +++++++++++++++---- .../omniauth_callbacks_controller_spec.rb | 1 + 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 817d3281d..e4e3ef3b4 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -9,18 +9,19 @@ def openid_connect auth_service = GovOneAuthService.new(code: params['code']) tokens_response = auth_service.tokens - return error_redirect unless valid_tokens?(tokens_response) - id_token = tokens_response['id_token'] - session[:id_token] = id_token - gov_one_id = auth_service.decode_id_token(id_token)[0]['sub'] + id_token = auth_service.decode_id_token(tokens_response['id_token'])[0] + session[:id_token] = tokens_response['id_token'] + gov_one_id = id_token['sub'] + return error_redirect unless auth_service.valid_id_token?(id_token, session[:gov_one_auth_nonce]) user_info_response = auth_service.user_info(tokens_response['access_token']) email = user_info_response['email'] return error_redirect unless valid_user_info?(user_info_response) gov_user = User.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) + session.delete(:nonce) sign_in_and_redirect gov_user if gov_user end @@ -40,7 +41,7 @@ def valid_tokens?(tokens_response) # @param user_info_response [Hash] # @return [Boolean] def valid_user_info?(user_info_response) - user_info_response.present? && user_info_response['email'].present? + user_info_response.present? && user_info_response['email'].present? && user_info_response['sub'] == session[:id_token]['sub'] end # @return [nil] diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 9369fbbef..53c5b3048 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -6,11 +6,12 @@ def login_uri client_id: Rails.application.config.gov_one_client_id, response_type: 'code', scope: 'email openid', - nonce: SecureRandom.uuid, + nonce: SecureRandom.alphanumeric(25), state: SecureRandom.uuid, } session[:gov_one_auth_state] = params[:state] + session[:gov_one_auth_nonce] = params[:nonce] gov_one_uri(:login, params) end diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 786ec439c..6018bbb52 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -38,6 +38,15 @@ def tokens {} end + # @param token [String] + # @param nonce [String] + # @return [Boolean] + def valid_id_token?(token, nonce) + token['iss'] == "#{Rails.application.config.gov_one_base_uri}/" && + token['aud'] == Rails.application.config.gov_one_client_id && + token['nonce'] == nonce + end + # GET /userinfo # @param access_token [String] # @return [Hash] @@ -66,7 +75,7 @@ def decode_id_token(token) key_params = jwks['keys'].find { |key| key['kid'] == kid } jwk = JWT::JWK.new(key_params) - JWT.decode(token, jwk.public_key, true, algorithm: 'ES256') + JWT.decode(token, jwk.public_key, true, { verify_iat: true, algorithm: 'ES256' }) end # @param address [String] @@ -80,11 +89,18 @@ def build_http(address) private + # @return [Hash] + def fetch_and_cache_jwks + Rails.cache.fetch('jwks', expires_in: 24.hours) do + uri, http = build_http(ENDPOINTS[:jwks]) + response = http.request(Net::HTTP::Get.new(uri.path)) + JSON.parse(response.body) + end + end + # @return [Hash] def jwks - uri, http = build_http(ENDPOINTS[:jwks]) - response = http.request(Net::HTTP::Get.new(uri.path)) - JSON.parse(response.body) + @jwks ||= fetch_and_cache_jwks end # @return [String] diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 0cf73f971..173221a15 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -18,6 +18,7 @@ allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') allow(auth_service).to receive(:decode_id_token).and_return([{ 'sub' => 'mock_sub' }]) + allow(auth_service).to receive(:valid_id_token?).and_return(true) end context 'with a new user' do From 6feb32675031de7bf7fdee654522cc61d19ec237 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 21 Nov 2023 09:41:28 +0000 Subject: [PATCH 071/122] check for session state and nonce on one-login redirect --- .../users/omniauth_callbacks_controller.rb | 17 +++++++++++++++-- app/services/gov_one_auth_service.rb | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index e4e3ef3b4..4302dfdb7 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -5,7 +5,7 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # This method is called by Devise after successful Gov One Login authentication # @return [nil] def openid_connect - session.delete(:gov_one_auth_state) + return unless session_params? && valid_params? auth_service = GovOneAuthService.new(code: params['code']) tokens_response = auth_service.tokens @@ -21,7 +21,8 @@ def openid_connect return error_redirect unless valid_user_info?(user_info_response) gov_user = User.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) - session.delete(:nonce) + + delete_session_params sign_in_and_redirect gov_user if gov_user end @@ -32,6 +33,12 @@ def valid_params? params['code'].present? && params['state'].present? && params['state'] == session[:gov_one_auth_state] end + # check state and nonce are saved in session + # @return [Boolean] + def session_params? + session[:gov_one_auth_state].present? && session[:gov_one_auth_nonce].present? + end + # @param tokens_response [Hash] # @return [Boolean] def valid_tokens?(tokens_response) @@ -50,6 +57,12 @@ def error_redirect redirect_to root_path end + # @return [nil] + def delete_session_params + session.delete(:gov_one_auth_state) + session.delete(:gov_one_auth_nonce) + end + # @return [String] def after_sign_in_path_for(resource) if resource.registration_complete? diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 6018bbb52..1b06383a2 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -89,6 +89,7 @@ def build_http(address) private + # GET /.well-known/jwks.json # @return [Hash] def fetch_and_cache_jwks Rails.cache.fetch('jwks', expires_in: 24.hours) do From 771550c1cfd0c40e3abaa7ca4149da1fadb58400 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 21 Nov 2023 10:03:51 +0000 Subject: [PATCH 072/122] mock session nonce in controller spec --- spec/controllers/users/omniauth_callbacks_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 173221a15..339497fa4 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -12,6 +12,7 @@ before do request.env['devise.mapping'] = Devise.mappings[:user] session[:gov_one_auth_state] = 'mock_state' + session[:gov_one_auth_nonce] = 'mock_nonce' allow(GovOneAuthService).to receive(:new).and_return(auth_service) allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) From a484a8364b98520aa2451a7f4cf356969163e9a3 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 21 Nov 2023 10:32:49 +0000 Subject: [PATCH 073/122] rubocop --- app/controllers/users/omniauth_callbacks_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 4302dfdb7..2e6f0d920 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -21,7 +21,7 @@ def openid_connect return error_redirect unless valid_user_info?(user_info_response) gov_user = User.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) - + delete_session_params sign_in_and_redirect gov_user if gov_user end From f781f1914858ac7d0c98d8b2cda430e4e54e2c31 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 21 Nov 2023 15:34:13 +0000 Subject: [PATCH 074/122] update account page for one login move --- app/assets/stylesheets/application.scss | 4 ++++ app/views/user/show.html.slim | 10 ++-------- config/locales/en.yml | 1 + spec/lib/seed_snippets_spec.rb | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index a87d09295..d1704f7c4 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -194,3 +194,7 @@ ul>li>ul>li { .white-space-pre-wrap { white-space: pre-wrap; } + +.text-secondary { + color: $govuk-secondary-text-colour !important; +} diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 1cf9f3e03..b0ae1b03e 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -14,14 +14,8 @@ - row.with_key { 'Name' } - row.with_value(text: current_user.name, classes: %w[data-hj-suppress]) - row.with_action(text: 'Change name', href: edit_registration_name_path, html_attributes: { id: :edit_name_registration }) - - your_details.with_row do |row| - - row.with_key { 'Email' } - - row.with_value(text:current_user.email, classes: %w[data-hj-suppress]) - - row.with_action(text: 'Change email', href: edit_email_user_path, html_attributes: { id: :edit_email_user }) - - your_details.with_row do |row| - - row.with_key { 'Password' } - - row.with_value { t('my_account.password_changed', date: current_user.password_last_changed) } - - row.with_action(text: 'Change password', href: edit_password_user_path, html_attributes: { id: :edit_password_user }) + p.text-secondary + = t('my_account.name_information') = govuk_summary_list do |other_details| - other_details.with_row do |row| diff --git a/config/locales/en.yml b/config/locales/en.yml index ec06dafcc..a3ad60362 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -296,6 +296,7 @@ en: # /my-account my_account: title: My account + name_information: This is the name that will appear on you end of module certificate. You can use this setting to change how your name appears. Changing your name on this account will not affect your Gov.UK One Login account. setting_details: | ## Your setting details email_preferences: | diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 5041dca24..bac2ac9a8 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 191 + expect(locales.count).to be 192 end it 'dot separated key -> Page::Resource#name' do From 26e7c5fe243ae918b7de0e240f349db299f9c3fe Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 21 Nov 2023 15:38:09 +0000 Subject: [PATCH 075/122] css update --- app/assets/stylesheets/application.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d1704f7c4..041d1e8a9 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -196,5 +196,5 @@ ul>li>ul>li { } .text-secondary { - color: $govuk-secondary-text-colour !important; + color: $govuk-secondary-text-colour; } From 7f29b86088cfe740ba28ab04e0ff25070bf26268 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 22 Nov 2023 10:03:22 +0000 Subject: [PATCH 076/122] remove change password spec --- .../registered_user/changing_password_spec.rb | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 spec/system/registered_user/changing_password_spec.rb diff --git a/spec/system/registered_user/changing_password_spec.rb b/spec/system/registered_user/changing_password_spec.rb deleted file mode 100644 index 47679ba47..000000000 --- a/spec/system/registered_user/changing_password_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Registered user changing password', type: :system do - subject(:user) { create :user, :registered, created_at: 1.month.ago } - - let(:password) { 'Str0ngPa$$w0rd' } - - include_context 'with user' - - before do - visit '/my-account/edit-password' - fill_in 'Enter your current password', with: 'Str0ngPa$$w0rd' - fill_in 'Create a new password', with: password - fill_in 'Confirm password', with: password - end - - context 'when cancelled' do - it 'returns to account page' do - click_link 'Cancel' - expect(page).to have_current_path '/my-account' - expect(page).not_to have_text 'Your new password has been saved.' - end - end - - context 'when successful' do - let(:password) { '12!@NewPassword' } - let(:today) { Time.zone.today.to_formatted_s(:rfc822) } # 18 May 2022 - - it 'updates password' do - click_button 'Save' - expect(page).to have_current_path '/my-account' - expect(page).to have_text('Manage your account') # page heading - .and have_text('Your new password has been saved.') # flash message - .and have_text("Password last changed on #{today}") # event - end - end - - context 'when too short' do - let(:password) { 'short' } - - it 'renders an error message' do - click_button 'Save' - expect(page).to have_text 'Password must be at least 10 characters.' - end - end - - context 'when blank' do - let(:password) { '' } - - it 'renders an error message' do - click_button 'Save' - expect(page).to have_text 'Enter a password.' - end - end -end From b81ec3832a89e89cc5d5dba95c64773d598b4af7 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 22 Nov 2023 11:58:39 +0000 Subject: [PATCH 077/122] update bot spec --- spec/requests/bot_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/requests/bot_spec.rb b/spec/requests/bot_spec.rb index 7f46679c8..3a7109570 100644 --- a/spec/requests/bot_spec.rb +++ b/spec/requests/bot_spec.rb @@ -2,9 +2,10 @@ RSpec.describe 'Automated bot', type: :request do let(:bot_email) { 'bot_token@example.com' } + let(:bot_name) { 'Bot' } before do - create :user, :registered, email: bot_email + create :user, :registered, email: bot_email, first_name: bot_name end context 'with header' do @@ -14,7 +15,7 @@ it 'is not redirected and can access secure pages' do expect(response).not_to redirect_to new_user_session_path - expect(response.body).to include bot_email + expect(response.body).to include bot_name end end From 4a353bc8a613d1de1fb9e6b9ad26f6be4bf7a54d Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 23 Nov 2023 13:44:46 +0000 Subject: [PATCH 078/122] minor styling updates --- app/views/user/show.html.slim | 2 +- config/locales/en.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index b0ae1b03e..39f273dcc 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -7,7 +7,7 @@ .govuk-grid-column-full h1.govuk-heading-l Manage your account h2.govuk-heading-m Your details - hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible + hr.govuk-section-break.govuk-section-break--l.govuk-section-break--visible class="govuk-!-margin-bottom-0" = govuk_summary_list do |your_details| - your_details.with_row do |row| diff --git a/config/locales/en.yml b/config/locales/en.yml index a3ad60362..c4bdb22a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -296,7 +296,7 @@ en: # /my-account my_account: title: My account - name_information: This is the name that will appear on you end of module certificate. You can use this setting to change how your name appears. Changing your name on this account will not affect your Gov.UK One Login account. + name_information: This is the name that will appear on you end of module certificate. You can use this setting to change how your name appears. Changing your name on this account will not affect your GOV.UK One Login account. setting_details: | ## Your setting details email_preferences: | From 16bb1583d4df1660861c0b6bf416d8333fc3cb7a Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 27 Nov 2023 09:43:45 +0000 Subject: [PATCH 079/122] add terms and conditions page --- .../terms_and_conditions_controller.rb | 35 +++++++++++++++++++ .../registration/terms_and_conditions_form.rb | 14 ++++++++ .../terms_and_conditions/edit.html.slim | 22 ++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 app/controllers/registration/terms_and_conditions_controller.rb create mode 100644 app/forms/registration/terms_and_conditions_form.rb create mode 100644 app/views/registration/terms_and_conditions/edit.html.slim diff --git a/app/controllers/registration/terms_and_conditions_controller.rb b/app/controllers/registration/terms_and_conditions_controller.rb new file mode 100644 index 000000000..79166cabf --- /dev/null +++ b/app/controllers/registration/terms_and_conditions_controller.rb @@ -0,0 +1,35 @@ +module Registration + class TermsAndConditionsController < BaseController + def edit; end + + def update + form.terms_and_conditions_agreed_at = user_params[:terms_and_conditions_agreed_at] + + if form.save + if current_user.registration_complete? + redirect_to user_path, notice: t(:details_updated) + else + redirect_to edit_registration_name_path + end + else + render :edit, status: :unprocessable_entity + end + end + + private + + # @return [Hash] + def user_params + params.require(:user).permit(:terms_and_conditions_agreed_at) + end + + # @return [Registration::NameForm] + def form + @form ||= + TermsAndConditionsForm.new( + user: current_user, + terms_and_conditions_agreed_at: current_user.terms_and_conditions_agreed_at, + ) + end + end +end diff --git a/app/forms/registration/terms_and_conditions_form.rb b/app/forms/registration/terms_and_conditions_form.rb new file mode 100644 index 000000000..e0e0c3a7a --- /dev/null +++ b/app/forms/registration/terms_and_conditions_form.rb @@ -0,0 +1,14 @@ +module Registration + class TermsAndConditionsForm < BaseForm + attr_accessor :terms_and_conditions_agreed_at + + validates :terms_and_conditions_agreed_at, presence: true + + # @return [Boolean] + def save + return false unless valid? + + user.update!(terms_and_conditions_agreed_at: terms_and_conditions_agreed_at) + end + end +end diff --git a/app/views/registration/terms_and_conditions/edit.html.slim b/app/views/registration/terms_and_conditions/edit.html.slim new file mode 100644 index 000000000..4d0e4f1a3 --- /dev/null +++ b/app/views/registration/terms_and_conditions/edit.html.slim @@ -0,0 +1,22 @@ += render 'user/debug' + +- content_for :page_title do + = html_title 'Terms and Conditions' + +.govuk-grid-row + .govuk-grid-column-two-thirds-from-desktop + = form_for form, url: registration_terms_and_conditions_path, method: :patch do |f| + = f.govuk_error_summary + + h1.govuk-heading-l = t('register_terms_and_conditions.heading') + + h3 = t('register_terms_and_conditions.subheading') + + = f.govuk_check_boxes_fieldset :terms_and_conditions_agreed_at, + legend: { class: 'govuk-visually-hidden', text: 'Terms and conditions'}, classes: 'light-grey-box' do + = m('register_terms_and_conditions.legend') + = f.terms_and_conditions_check_box + + + .govuk-button-group + = f.govuk_submit t('links.continue') From f2ead426c0950189f7323b10292c344c368927c2 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 27 Nov 2023 11:18:05 +0000 Subject: [PATCH 080/122] update localisations and specs --- app/controllers/application_controller.rb | 2 +- .../users/omniauth_callbacks_controller.rb | 4 ++-- app/controllers/users/sessions_controller.rb | 2 +- config/locales/en.yml | 7 +++++++ config/routes.rb | 1 + .../users/omniauth_callbacks_controller_spec.rb | 13 +++++++------ spec/lib/seed_snippets_spec.rb | 2 +- .../confirmed_user/completing_registration_spec.rb | 12 ++++++++++++ 8 files changed, 32 insertions(+), 11 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b7ca4f15e..7b9ba514a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,7 +20,7 @@ def authenticate_registered_user! authenticate_user! unless user_signed_in? return true if current_user.registration_complete? - redirect_to edit_registration_name_path, notice: 'Please complete registration' + redirect_to edit_registration_terms_and_conditions_path, notice: 'Please complete registration' end def configure_permitted_parameters diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 2e6f0d920..2a3d189aa 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -12,7 +12,7 @@ def openid_connect return error_redirect unless valid_tokens?(tokens_response) id_token = auth_service.decode_id_token(tokens_response['id_token'])[0] - session[:id_token] = tokens_response['id_token'] + session[:id_token] = id_token gov_one_id = id_token['sub'] return error_redirect unless auth_service.valid_id_token?(id_token, session[:gov_one_auth_nonce]) @@ -78,7 +78,7 @@ def after_sign_in_path_for(resource) elsif resource.private_beta_registration_complete? static_path('new-registration') else - edit_registration_name_path + edit_registration_terms_and_conditions_path end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index d70bff7ee..a4db408cb 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -17,7 +17,7 @@ def after_sign_in_path_for(resource) elsif resource.private_beta_registration_complete? static_path('new-registration') else - edit_registration_name_path + edit_registration_terms_and_conditions_path end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index ec06dafcc..f04bc9b73 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -449,6 +449,13 @@ en: complete_registration: Thank you for creating an Early years child development training account. You can now start your first module. update_registration: Thank you for updating your Early years child development training account. You can now continue. + # /registration/terms-and-conditions/edit + register_terms_and_conditions: + heading: Set up your training account + subheading: Agree to our terms and conditions + legend: | + To use this service, you must accept the [terms and conditions](/terms-and-conditions) and [privacy policy](/privacy-policy). + # /registration/name/edit register_name: heading: About you diff --git a/config/routes.rb b/config/routes.rb index 9de3c4b5c..ec9655633 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,6 +37,7 @@ end namespace :registration do + resource :terms_and_conditions, only: %i[edit update], path: 'terms-and-conditions' resource :name, only: %i[edit update] resource :setting_type, only: %i[edit update], path: 'setting-type' resource :setting_type_other, only: %i[edit update], path: 'setting-type-other' diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 339497fa4..132d453b9 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -4,6 +4,7 @@ let(:auth_service) { instance_double(GovOneAuthService) } let(:access_token) { 'mock_access_token' } let(:id_token) { 'mock_id_token' } + let(:decoded_id_token) { { 'sub' => 'mock_sub' } } let(:email) { 'test@example.com' } let(:params) do { 'code' => 'mock_code', 'state' => 'mock_state' } @@ -16,9 +17,9 @@ allow(GovOneAuthService).to receive(:new).and_return(auth_service) allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) - allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) + allow(auth_service).to receive(:user_info).and_return({ 'email' => email, 'sub' => 'mock_sub' }) allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') - allow(auth_service).to receive(:decode_id_token).and_return([{ 'sub' => 'mock_sub' }]) + allow(auth_service).to receive(:decode_id_token).and_return([decoded_id_token]) allow(auth_service).to receive(:valid_id_token?).and_return(true) end @@ -33,8 +34,8 @@ end it 'redirects to complete registration' do - expect(session[:id_token]).to eq id_token - expect(response).to redirect_to edit_registration_name_path + expect(session[:id_token]).to eq decoded_id_token + expect(response).to redirect_to edit_registration_terms_and_conditions_path end end @@ -49,7 +50,7 @@ end it 'redirects to /my-modules' do - expect(session[:id_token]).to eq id_token + expect(session[:id_token]).to eq decoded_id_token expect(response).to redirect_to my_modules_path end end @@ -61,7 +62,7 @@ end it 'redirects to /my-modules' do - expect(session[:id_token]).to eq id_token + expect(session[:id_token]).to eq decoded_id_token expect(response).to redirect_to my_modules_path end end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 5041dca24..fbcf787bf 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 191 + expect(locales.count).to be 194 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/system/confirmed_user/completing_registration_spec.rb b/spec/system/confirmed_user/completing_registration_spec.rb index 420391fb0..bcd43c72b 100644 --- a/spec/system/confirmed_user/completing_registration_spec.rb +++ b/spec/system/confirmed_user/completing_registration_spec.rb @@ -6,6 +6,18 @@ let(:user) { create :user, :confirmed } it 'requires name and a setting type and email preferences and a complete' do + expect(page).to have_text('Terms and conditions') + + click_button 'Continue' + + expect(page).to have_text('There is a problem') + .and have_text('You must accept the terms and conditions and privacy policy to create an account.') + + expect(page).to have_text('Agree to our terms and conditions') + + check 'I confirm that I accept the terms and conditions and privacy policy.' + click_button 'Continue' + expect(page).to have_text('About you') click_button 'Continue' From 85baa09867c5622211ab676233cc60150a685552 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 27 Nov 2023 11:22:02 +0000 Subject: [PATCH 081/122] update callbacks controller --- .../users/omniauth_callbacks_controller.rb | 2 +- .../users/omniauth_callbacks_controller_spec.rb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 2e6f0d920..66a611c2a 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -12,7 +12,7 @@ def openid_connect return error_redirect unless valid_tokens?(tokens_response) id_token = auth_service.decode_id_token(tokens_response['id_token'])[0] - session[:id_token] = tokens_response['id_token'] + session[:id_token] = id_token gov_one_id = id_token['sub'] return error_redirect unless auth_service.valid_id_token?(id_token, session[:gov_one_auth_nonce]) diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index 339497fa4..8b7fb7682 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -4,6 +4,7 @@ let(:auth_service) { instance_double(GovOneAuthService) } let(:access_token) { 'mock_access_token' } let(:id_token) { 'mock_id_token' } + let(:decoded_id_token) { { 'sub' => 'mock_sub' } } let(:email) { 'test@example.com' } let(:params) do { 'code' => 'mock_code', 'state' => 'mock_state' } @@ -16,9 +17,9 @@ allow(GovOneAuthService).to receive(:new).and_return(auth_service) allow(auth_service).to receive(:tokens).and_return({ 'access_token' => access_token, 'id_token' => id_token }) - allow(auth_service).to receive(:user_info).and_return({ 'email' => email }) + allow(auth_service).to receive(:user_info).and_return({ 'email' => email, 'sub' => 'mock_sub' }) allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') - allow(auth_service).to receive(:decode_id_token).and_return([{ 'sub' => 'mock_sub' }]) + allow(auth_service).to receive(:decode_id_token).and_return([decoded_id_token]) allow(auth_service).to receive(:valid_id_token?).and_return(true) end @@ -33,7 +34,7 @@ end it 'redirects to complete registration' do - expect(session[:id_token]).to eq id_token + expect(session[:id_token]).to eq decoded_id_token expect(response).to redirect_to edit_registration_name_path end end @@ -49,7 +50,7 @@ end it 'redirects to /my-modules' do - expect(session[:id_token]).to eq id_token + expect(session[:id_token]).to eq decoded_id_token expect(response).to redirect_to my_modules_path end end @@ -61,7 +62,7 @@ end it 'redirects to /my-modules' do - expect(session[:id_token]).to eq id_token + expect(session[:id_token]).to eq decoded_id_token expect(response).to redirect_to my_modules_path end end From 28f80d561d0567e17d8ef83053d82a2393bf5ab1 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 27 Nov 2023 17:14:57 +0000 Subject: [PATCH 082/122] update specs --- config/sitemap.rb | 1 + spec/requests/authentication_spec.rb | 4 ++-- spec/system/sign_in_spec.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/sitemap.rb b/config/sitemap.rb index 901358c90..733b813ea 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -71,6 +71,7 @@ # edit registration/account add edit_email_user_path add edit_password_user_path + add edit_registration_terms_and_conditions_path add edit_registration_name_path add edit_registration_setting_type_path add edit_registration_setting_type_other_path diff --git a/spec/requests/authentication_spec.rb b/spec/requests/authentication_spec.rb index 66da4ccf2..d2ad5bb74 100644 --- a/spec/requests/authentication_spec.rb +++ b/spec/requests/authentication_spec.rb @@ -4,7 +4,7 @@ # RSpec.describe 'Authentication', type: :request do describe 'viewing authenticate_user! controller action' do - let(:action_path) { edit_registration_name_path } + let(:action_path) { edit_registration_terms_and_conditions_path } context 'with User not signed in' do it 'redirects to sign in page' do @@ -89,7 +89,7 @@ it 'redirects to finish registration' do get action_path - expect(response).to redirect_to(edit_registration_name_path) + expect(response).to redirect_to(edit_registration_terms_and_conditions_path) end it 'displays message to complete registration' do diff --git a/spec/system/sign_in_spec.rb b/spec/system/sign_in_spec.rb index a55ae8c2c..3cd4eec9e 100644 --- a/spec/system/sign_in_spec.rb +++ b/spec/system/sign_in_spec.rb @@ -45,7 +45,7 @@ context 'and enters valid credentials' do it 'signs in successfully' do - expect(page).to have_text('About you') # extra registration + expect(page).to have_text('Agree to our terms and conditions') # extra registration end end From d50aed79df719f1729a540cfcbe1286828726558 Mon Sep 17 00:00:00 2001 From: Peter David Hamilton <pete@peterdavidhamilton.com> Date: Thu, 30 Nov 2023 12:32:25 +0000 Subject: [PATCH 083/122] Refactor .find_or_create_from_gov_one spec for dry lazy loading User .find_or_create_from_gov_one with an existing account and using GovOne with a new email updates email and using GovOne for the first time associates GovOne ID without an existing account creates a new user --- spec/models/user_spec.rb | 61 +++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c1b68455c..f43490f5c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -208,43 +208,52 @@ end describe '.find_or_create_from_gov_one' do - subject(:find_or_create) { described_class.find_or_create_from_gov_one(email: email, gov_one_id: gov_one_id) } + let(:email) { 'current@test.com' } + let(:gov_one_id) { 'urn:fdc:gov.uk:2022:23-random-alpha-numeric' } - let(:email) { 'test@test.com' } - let(:gov_one_id) { '123' } - let(:user) { create(:user) } + before do + described_class.find_or_create_from_gov_one(**params) + end - context 'with an existing user having an email but no gov_one_id' do - before do - user.update_column(:email, email) - find_or_create + context 'without an existing account' do + let(:params) do + { email: email, gov_one_id: gov_one_id } end - it 'updates the user gov_one_id' do - expect(user.reload.gov_one_id).to eq(gov_one_id) + it 'creates a new user' do + expect(described_class.count).to eq 1 + expect(described_class.first.email).to eq params[:email] + expect(described_class.first.gov_one_id).to eq params[:gov_one_id] end end - context 'with an existing user having a gov_one_id' do - before do - user.update_column(:gov_one_id, gov_one_id) - find_or_create - end + context 'with an existing account' do + context 'and using GovOne for the first time' do + let(:user) do + create :user, :registered, email: email + end - it 'updates the user email' do - expect(user.reload.email).to eq(email) - end - end + let(:params) do + { email: user.email, gov_one_id: gov_one_id } + end - context 'without an existing user' do - before do - find_or_create + it 'associates GovOne ID' do + expect(user.reload.gov_one_id).to eq gov_one_id + end end - it 'creates a new user' do - expect(described_class.count).to eq(1) - expect(described_class.first.email).to eq(email) - expect(described_class.first.gov_one_id).to eq(gov_one_id) + context 'and using GovOne with a new email' do + let(:user) do + create :user, :registered, email: 'old@test.com', gov_one_id: gov_one_id + end + + let(:params) do + { email: email, gov_one_id: user.gov_one_id } + end + + it 'updates email' do + expect(user.reload.email).to eq email + end end end end From f529817ed47861cb87830f68cc27a34b599f3605 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 1 Dec 2023 08:54:32 +0000 Subject: [PATCH 084/122] refactor gov one jwks to remove memoization --- app/services/gov_one_auth_service.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index 1b06383a2..c212865ec 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -91,7 +91,7 @@ def build_http(address) # GET /.well-known/jwks.json # @return [Hash] - def fetch_and_cache_jwks + def jwks Rails.cache.fetch('jwks', expires_in: 24.hours) do uri, http = build_http(ENDPOINTS[:jwks]) response = http.request(Net::HTTP::Get.new(uri.path)) @@ -99,11 +99,6 @@ def fetch_and_cache_jwks end end - # @return [Hash] - def jwks - @jwks ||= fetch_and_cache_jwks - end - # @return [String] def jwt_assertion rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.gov_one_private_key) From dad5b3ae3d771d56e8017eefb2832996de4f9b87 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 1 Dec 2023 15:47:51 +0000 Subject: [PATCH 085/122] add feature flag to redirect --- app/controllers/application_controller.rb | 6 +++++- app/controllers/users/sessions_controller.rb | 4 +++- config/application.rb | 5 +++++ .../confirmed_user/completing_registration_spec.rb | 9 ++++----- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7b9ba514a..3dd694581 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,7 +20,11 @@ def authenticate_registered_user! authenticate_user! unless user_signed_in? return true if current_user.registration_complete? - redirect_to edit_registration_terms_and_conditions_path, notice: 'Please complete registration' + if Rails.application.gov_one_login? + redirect_to edit_registration_terms_and_conditions_path, notice: 'Please complete registration' + else + redirect_to edit_registration_name_path, notice: 'Please complete registration' + end end def configure_permitted_parameters diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index a4db408cb..aeb2584a5 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -16,8 +16,10 @@ def after_sign_in_path_for(resource) end elsif resource.private_beta_registration_complete? static_path('new-registration') - else + elsif Rails.application.gov_one_login? edit_registration_terms_and_conditions_path + else + edit_registration_name_path end end end diff --git a/config/application.rb b/config/application.rb index 42c3e1478..8a0b7345f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -101,6 +101,11 @@ def maintenance? Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] end + # @return [Boolean] + def gov_one_login? + Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] + end + # @return [ActiveSupport::TimeWithZone] def public_beta_launch_date Time.zone.local(2023, 2, 9, 15, 0, 0) diff --git a/spec/system/confirmed_user/completing_registration_spec.rb b/spec/system/confirmed_user/completing_registration_spec.rb index bcd43c72b..105e843c9 100644 --- a/spec/system/confirmed_user/completing_registration_spec.rb +++ b/spec/system/confirmed_user/completing_registration_spec.rb @@ -1,15 +1,16 @@ require 'rails_helper' RSpec.describe 'Confirmed users completing registration' do - include_context 'with user' + before do + allow(Rails.application).to receive(:gov_one_login?).and_return(true) + end + include_context 'with user' let(:user) { create :user, :confirmed } it 'requires name and a setting type and email preferences and a complete' do expect(page).to have_text('Terms and conditions') - click_button 'Continue' - expect(page).to have_text('There is a problem') .and have_text('You must accept the terms and conditions and privacy policy to create an account.') @@ -60,14 +61,12 @@ expect(page).to have_text('What is your role?') .and have_text('Enter your job title.') - click_button 'Continue' expect(page).to have_text('There is a problem') .and have_text('Enter your job title.') fill_in 'Enter your job title.', with: 'user defined job title' - click_button 'Continue' expect(page).to have_text('Do you want to get email updates about this training course?') From 1c9fd7c632f87d0cdbe0e7958018dd38b9b9f217 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 1 Dec 2023 16:05:42 +0000 Subject: [PATCH 086/122] update sign_in_spec --- spec/system/sign_in_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/system/sign_in_spec.rb b/spec/system/sign_in_spec.rb index 3cd4eec9e..9480f0168 100644 --- a/spec/system/sign_in_spec.rb +++ b/spec/system/sign_in_spec.rb @@ -43,6 +43,10 @@ context 'when user is confirmed' do let(:user) { create :user, :confirmed } + before do + allow(Rails.application).to receive(:gov_one_login?).and_return(true) + end + context 'and enters valid credentials' do it 'signs in successfully' do expect(page).to have_text('Agree to our terms and conditions') # extra registration From 0c6fa4691a70ecf208674e6eaceb3e7df122714b Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 4 Dec 2023 11:22:49 +0000 Subject: [PATCH 087/122] update locales, rename controller method --- app/assets/stylesheets/application.scss | 12 ----------- app/controllers/gov_one_controller.rb | 2 +- app/helpers/gov_one_helper.rb | 2 +- app/views/gov_one/info.html.slim | 28 ------------------------- app/views/gov_one/show.html.slim | 14 +++++++++++++ config/locales/en.yml | 5 ++--- config/routes.rb | 2 +- spec/system/whats_new_page_spec.rb | 2 -- 8 files changed, 19 insertions(+), 48 deletions(-) delete mode 100644 app/views/gov_one/info.html.slim create mode 100644 app/views/gov_one/show.html.slim diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index a87d09295..632edbb79 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -135,14 +135,6 @@ pre.code-tip { margin-bottom: govuk-spacing(6); } - &.enrol-box { - border-radius: 6px; - - .govuk-button { - margin: 0; - } - } - } .float-right { @@ -190,7 +182,3 @@ ul>li>ul>li { .govuk-footer__copyright-logo { display: none; } - -.white-space-pre-wrap { - white-space: pre-wrap; -} diff --git a/app/controllers/gov_one_controller.rb b/app/controllers/gov_one_controller.rb index de6d437dd..25faf07d0 100644 --- a/app/controllers/gov_one_controller.rb +++ b/app/controllers/gov_one_controller.rb @@ -1,7 +1,7 @@ class GovOneController < ApplicationController layout 'hero' - def info + def show redirect_to my_modules_path if current_user end end diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index f24c16022..869ce4637 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -28,7 +28,7 @@ def logout_uri # @return [String] def login_button - govuk_button_link_to t('gov_one_info.sign_in_button'), login_uri.to_s + govuk_button_link_to t('gov_one_info.button.sign_in'), login_uri.to_s end private diff --git a/app/views/gov_one/info.html.slim b/app/views/gov_one/info.html.slim deleted file mode 100644 index f50a52ce6..000000000 --- a/app/views/gov_one/info.html.slim +++ /dev/null @@ -1,28 +0,0 @@ -- content_for :page_title do - = html_title t('home.title') - -- content_for :hero do - .govuk-grid-row class='govuk-!-padding-top-9' - .govuk-grid-column-three-quarters - h1.dfe-heading-xl class='govuk-!-margin-bottom-4' - = t('gov_one_info.hero_header') - p.govuk-body-l = t('gov_one_info.hero_body') - - -.govuk-grid-row - .govuk-grid-column-full - . class='govuk-!-margin-bottom-5' - = m('gov_one_info.body') - hr - - = login_button - -.govuk-grid-row - .govuk-grid-column-three-quarters class='govuk-!-margin-top-4 govuk-!-margin-bottom-9' - details.govuk-details data-module='govuk-details' - summary.govuk-details__summary - span.govuk-details__summary-text - = t('gov_one_info.details_summary') - .govuk-details__text - = t('gov_one_info.details_text') - \ No newline at end of file diff --git a/app/views/gov_one/show.html.slim b/app/views/gov_one/show.html.slim new file mode 100644 index 000000000..4fd836421 --- /dev/null +++ b/app/views/gov_one/show.html.slim @@ -0,0 +1,14 @@ += render 'learning/cms_debug' + +- content_for :page_title do + = html_title t('gov_one_info.title') + +.govuk-grid-row + .govuk-grid-column-one-half + = m('gov_one_info.body') + + - if current_user + = logout_button + - else + = login_button + \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index cce7aa516..744e0866c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -508,8 +508,6 @@ en: home: title: Home page hero: | - # %{service_name} - This free, online training provides an overview of child development and gives practical advice for supporting the development of children in your early years setting. thumb: |  @@ -581,7 +579,8 @@ en: hero_header: How to access this training course hero_body: This service uses GOV.UK One Login which is managed by the Government Digital Service. body: You will be asked to sign in to your account, or create a One Login account, in this service - sign_in_button: Continue to GOV.UK One Login + button: + sign_in: Continue to GOV.UK One Login details_summary: How to access an existing training account details_text: If you have an existing early years child development training account but you do not yet have a GOV.UK One Login, you must use the same email address for both accounts. This will ensure that any progress you have made through the training is retained. diff --git a/config/routes.rb b/config/routes.rb index 9de3c4b5c..eb38e312a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,7 @@ get 'my-modules', to: 'learning#show' # @see User#course get 'about-training', to: 'training/modules#index', as: :course_overview - get 'gov-one/info', to: 'gov_one#info' + get 'gov-one/info', to: 'gov_one#show' get '/404', to: 'errors#not_found', via: :all get '/422', to: 'errors#unprocessable_entity', via: :all diff --git a/spec/system/whats_new_page_spec.rb b/spec/system/whats_new_page_spec.rb index 1a86631ff..07685bd52 100644 --- a/spec/system/whats_new_page_spec.rb +++ b/spec/system/whats_new_page_spec.rb @@ -20,7 +20,6 @@ end describe 'with subsequent logins' do - skip 'wip' do before do click_on 'sign-out-desktop' @@ -36,5 +35,4 @@ end end end - end end From 948fa98606f6487d309ced1f57de4a46e142a44f Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 4 Dec 2023 11:57:12 +0000 Subject: [PATCH 088/122] use feature flag in application_helper links --- app/helpers/application_helper.rb | 18 +++++++++++---- app/views/gov_one/show.html.slim | 36 +++++++++++++++++++++--------- config/application.rb | 5 +++++ spec/system/gov_one_info_spec.rb | 10 ++++----- spec/system/whats_new_page_spec.rb | 22 +++++++++--------- 5 files changed, 60 insertions(+), 31 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1d4c66edb..a892f5b4a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,14 +11,14 @@ def navigation header.with_navigation_item(text: 'Home', href: root_path, classes: %w[dfe-header__navigation-item]) if user_signed_in? header.with_action_link(text: 'My Account', href: user_path, options: { inverse: true }) - header.with_action_link(text: 'Sign out', href: logout_uri.to_s, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) + header.with_action_link(text: 'Sign out', href: logout_path, options: { id: 'sign-out-desktop', data: { turbo_method: :get }, inverse: true }) header.with_navigation_item(text: 'My modules', href: my_modules_path, classes: %w[dfe-header__navigation-item]) header.with_navigation_item(text: 'Learning log', href: user_notes_path, classes: %w[dfe-header__navigation-items]) if current_user.course_started? header.with_navigation_item(text: 'My account', href: user_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) - header.with_navigation_item(text: 'Sign out', href: logout_uri.to_s, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) + header.with_navigation_item(text: 'Sign out', href: logout_path, options: { data: { turbo_method: :get } }, classes: %w[dfe-header__navigation-item dfe-header-f-mob], html_attributes: { id: 'sign-out-f-mob' }) else - header.with_action_link(text: 'Sign in', href: gov_one_info_path, options: { inverse: true }) - header.with_navigation_item(text: 'Sign in', href: gov_one_info_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) + header.with_action_link(text: 'Sign in', href: login_path, options: { inverse: true }) + header.with_navigation_item(text: 'Sign in', href: login_path, classes: %w[dfe-header__navigation-item dfe-header-f-mob]) end end end @@ -59,4 +59,14 @@ def html_title(*parts) def calculate_module_state CalculateModuleState.new(user: current_user).call end + + # @return [String] + def login_path + Rails.application.gov_one_login? ? gov_one_info_path : new_user_session_path + end + + # @return [String] + def logout_path + Rails.application.gov_one_login? ? logout_uri.to_s : destroy_user_session_path + end end diff --git a/app/views/gov_one/show.html.slim b/app/views/gov_one/show.html.slim index 4fd836421..f50a52ce6 100644 --- a/app/views/gov_one/show.html.slim +++ b/app/views/gov_one/show.html.slim @@ -1,14 +1,28 @@ -= render 'learning/cms_debug' - - content_for :page_title do - = html_title t('gov_one_info.title') + = html_title t('home.title') + +- content_for :hero do + .govuk-grid-row class='govuk-!-padding-top-9' + .govuk-grid-column-three-quarters + h1.dfe-heading-xl class='govuk-!-margin-bottom-4' + = t('gov_one_info.hero_header') + p.govuk-body-l = t('gov_one_info.hero_body') + + +.govuk-grid-row + .govuk-grid-column-full + . class='govuk-!-margin-bottom-5' + = m('gov_one_info.body') + hr + + = login_button .govuk-grid-row - .govuk-grid-column-one-half - = m('gov_one_info.body') - - - if current_user - = logout_button - - else - = login_button - \ No newline at end of file + .govuk-grid-column-three-quarters class='govuk-!-margin-top-4 govuk-!-margin-bottom-9' + details.govuk-details data-module='govuk-details' + summary.govuk-details__summary + span.govuk-details__summary-text + = t('gov_one_info.details_summary') + .govuk-details__text + = t('gov_one_info.details_text') + \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 42c3e1478..8a0b7345f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -101,6 +101,11 @@ def maintenance? Types::Params::Bool[ENV.fetch('MAINTENANCE', false)] end + # @return [Boolean] + def gov_one_login? + Types::Params::Bool[ENV.fetch('GOV_ONE_LOGIN', false)] + end + # @return [ActiveSupport::TimeWithZone] def public_beta_launch_date Time.zone.local(2023, 2, 9, 15, 0, 0) diff --git a/spec/system/gov_one_info_spec.rb b/spec/system/gov_one_info_spec.rb index bb252a4b7..0d75df6ef 100644 --- a/spec/system/gov_one_info_spec.rb +++ b/spec/system/gov_one_info_spec.rb @@ -1,11 +1,12 @@ require 'rails_helper' RSpec.describe 'Gov One Info' do - context 'with an unauthenticated visitor' do - before do - visit '/gov-one/info' - end + before do + allow(Rails.application).to receive(:gov_one_login?).and_return(false) + visit '/gov-one/info' + end + context 'with an unauthenticated visitor' do it 'displays the correct content' do expect(page).to have_css('h1', text: 'How to access this training course') expect(page).to have_css('p', text: 'This service uses GOV.UK One Login which is managed by the Government Digital Service.') @@ -23,7 +24,6 @@ include_context 'with user' it 'redirects to the my modules page' do - visit '/gov-one/info' expect(page).to have_current_path('/my-modules') end end diff --git a/spec/system/whats_new_page_spec.rb b/spec/system/whats_new_page_spec.rb index 07685bd52..7fc194638 100644 --- a/spec/system/whats_new_page_spec.rb +++ b/spec/system/whats_new_page_spec.rb @@ -20,19 +20,19 @@ end describe 'with subsequent logins' do - before do - click_on 'sign-out-desktop' + before do + click_on 'sign-out-desktop' - visit '/users/sign-in' - fill_in 'Email address', with: user.email - fill_in 'Password', with: user.password - click_button 'Sign in' - end + visit '/users/sign-in' + fill_in 'Email address', with: user.email + fill_in 'Password', with: user.password + click_button 'Sign in' + end - it 'does not appear' do - expect(page).not_to have_current_path '/whats-new' - expect(page).to have_current_path '/my-modules' - end + it 'does not appear' do + expect(page).not_to have_current_path '/whats-new' + expect(page).to have_current_path '/my-modules' end end + end end From 932ba80b37ba8f01925694fa7f69488852aaad3b Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 4 Dec 2023 13:15:50 +0000 Subject: [PATCH 089/122] nest gov one info locales --- app/views/gov_one/show.html.slim | 4 ++-- config/locales/en.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/gov_one/show.html.slim b/app/views/gov_one/show.html.slim index f50a52ce6..f6e82ab15 100644 --- a/app/views/gov_one/show.html.slim +++ b/app/views/gov_one/show.html.slim @@ -5,8 +5,8 @@ .govuk-grid-row class='govuk-!-padding-top-9' .govuk-grid-column-three-quarters h1.dfe-heading-xl class='govuk-!-margin-bottom-4' - = t('gov_one_info.hero_header') - p.govuk-body-l = t('gov_one_info.hero_body') + = t('gov_one_info.hero.header') + p.govuk-body-l = t('gov_one_info.hero.body') .govuk-grid-row diff --git a/config/locales/en.yml b/config/locales/en.yml index 4e6760916..d944df393 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -578,8 +578,9 @@ en: # /gov-one/info gov_one_info: title: Gov One Info - hero_header: How to access this training course - hero_body: This service uses GOV.UK One Login which is managed by the Government Digital Service. + hero: + header: How to access this training course + body: This service uses GOV.UK One Login which is managed by the Government Digital Service. body: You will be asked to sign in to your account, or create a One Login account, in this service button: sign_in: Continue to GOV.UK One Login From 9c8bdda6073197a1b2acff021f9dce60d36a8778 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 4 Dec 2023 13:27:53 +0000 Subject: [PATCH 090/122] simplify gov_one_info spec --- spec/system/gov_one_info_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/gov_one_info_spec.rb b/spec/system/gov_one_info_spec.rb index 0d75df6ef..d5a87ff42 100644 --- a/spec/system/gov_one_info_spec.rb +++ b/spec/system/gov_one_info_spec.rb @@ -16,7 +16,7 @@ it 'has the correct login link' do link = find_link('Continue to GOV.UK One Login') - expect(link[:href]).to start_with('https://oidc.test.account.gov.uk/authorize?redirect_uri=http%3A%2F%2Frecovery.app%2Fusers%2Fauth%2Fopenid_connect%2Fcallback&client_id=some_client_id&response_type=code&scope=email+openid&nonce=') + expect(link[:href]).to start_with('https://oidc.test.account.gov.uk/authorize?') end end From a21cd788bdd5fe5a0ed88f7b6a597c2650dd2625 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 4 Dec 2023 13:33:49 +0000 Subject: [PATCH 091/122] remove link assertion from gov_one_info spec --- spec/system/gov_one_info_spec.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/system/gov_one_info_spec.rb b/spec/system/gov_one_info_spec.rb index d5a87ff42..4107f9c25 100644 --- a/spec/system/gov_one_info_spec.rb +++ b/spec/system/gov_one_info_spec.rb @@ -13,11 +13,6 @@ expect(page).to have_css('p', text: 'You will be asked to sign in to your account, or create a One Login account, in this service') expect(page).to have_css('a', text: 'Continue to GOV.UK One Login') end - - it 'has the correct login link' do - link = find_link('Continue to GOV.UK One Login') - expect(link[:href]).to start_with('https://oidc.test.account.gov.uk/authorize?') - end end context 'with an authenticated user' do From b915b6a66b5de260dd61047d41560f4d76d54e30 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 4 Dec 2023 17:05:14 +0000 Subject: [PATCH 092/122] update authentication_spec --- spec/requests/authentication_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/requests/authentication_spec.rb b/spec/requests/authentication_spec.rb index d2ad5bb74..466b3af5c 100644 --- a/spec/requests/authentication_spec.rb +++ b/spec/requests/authentication_spec.rb @@ -85,7 +85,10 @@ end context 'with partially registered User' do - before { sign_in create(:user, :confirmed) } + before do + allow(Rails.application).to receive(:gov_one_login?).and_return(true) + sign_in create(:user, :confirmed) + end it 'redirects to finish registration' do get action_path From d1b61eb89668ea2885cea826354b7a7a328e8417 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 5 Dec 2023 09:15:05 +0000 Subject: [PATCH 093/122] update sign_in system spec --- spec/system/sign_in_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/system/sign_in_spec.rb b/spec/system/sign_in_spec.rb index 9480f0168..b955ce97a 100644 --- a/spec/system/sign_in_spec.rb +++ b/spec/system/sign_in_spec.rb @@ -5,6 +5,7 @@ let(:password) { 'Str0ngPa$$w0rd' } before do + allow(Rails.application).to receive(:gov_one_login?).and_return(true) visit '/users/sign-in' fill_in 'Email address', with: email_address fill_in 'Password', with: password @@ -43,10 +44,6 @@ context 'when user is confirmed' do let(:user) { create :user, :confirmed } - before do - allow(Rails.application).to receive(:gov_one_login?).and_return(true) - end - context 'and enters valid credentials' do it 'signs in successfully' do expect(page).to have_text('Agree to our terms and conditions') # extra registration From 1f0dd1e46e8dbf976fec0da3170bd706088a7109 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 5 Dec 2023 10:10:33 +0000 Subject: [PATCH 094/122] wip --- spec/system/registered_user/changing_password_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/system/registered_user/changing_password_spec.rb b/spec/system/registered_user/changing_password_spec.rb index 47679ba47..e1e58782c 100644 --- a/spec/system/registered_user/changing_password_spec.rb +++ b/spec/system/registered_user/changing_password_spec.rb @@ -5,9 +5,11 @@ let(:password) { 'Str0ngPa$$w0rd' } + include_context 'with user' before do + puts user.email visit '/my-account/edit-password' fill_in 'Enter your current password', with: 'Str0ngPa$$w0rd' fill_in 'Create a new password', with: password From 3eb81be1e14b5c949e286a015b0df1cb11729bb2 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 5 Dec 2023 10:13:59 +0000 Subject: [PATCH 095/122] revert change_password spec changes --- .../registered_user/changing_password_spec.rb | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spec/system/registered_user/changing_password_spec.rb diff --git a/spec/system/registered_user/changing_password_spec.rb b/spec/system/registered_user/changing_password_spec.rb new file mode 100644 index 000000000..e1e58782c --- /dev/null +++ b/spec/system/registered_user/changing_password_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe 'Registered user changing password', type: :system do + subject(:user) { create :user, :registered, created_at: 1.month.ago } + + let(:password) { 'Str0ngPa$$w0rd' } + + + include_context 'with user' + + before do + puts user.email + visit '/my-account/edit-password' + fill_in 'Enter your current password', with: 'Str0ngPa$$w0rd' + fill_in 'Create a new password', with: password + fill_in 'Confirm password', with: password + end + + context 'when cancelled' do + it 'returns to account page' do + click_link 'Cancel' + expect(page).to have_current_path '/my-account' + expect(page).not_to have_text 'Your new password has been saved.' + end + end + + context 'when successful' do + let(:password) { '12!@NewPassword' } + let(:today) { Time.zone.today.to_formatted_s(:rfc822) } # 18 May 2022 + + it 'updates password' do + click_button 'Save' + expect(page).to have_current_path '/my-account' + expect(page).to have_text('Manage your account') # page heading + .and have_text('Your new password has been saved.') # flash message + .and have_text("Password last changed on #{today}") # event + end + end + + context 'when too short' do + let(:password) { 'short' } + + it 'renders an error message' do + click_button 'Save' + expect(page).to have_text 'Password must be at least 10 characters.' + end + end + + context 'when blank' do + let(:password) { '' } + + it 'renders an error message' do + click_button 'Save' + expect(page).to have_text 'Enter a password.' + end + end +end From 9df3ed6b3915fd29d2cef051ea79bedca35ce200 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 6 Dec 2023 10:23:06 +0000 Subject: [PATCH 096/122] update change_password spec --- app/helpers/gov_one_helper.rb | 2 -- spec/system/registered_user/changing_password_spec.rb | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/helpers/gov_one_helper.rb b/app/helpers/gov_one_helper.rb index 6e36bab6f..23cd93f05 100644 --- a/app/helpers/gov_one_helper.rb +++ b/app/helpers/gov_one_helper.rb @@ -18,8 +18,6 @@ def login_uri # @return [URI] def logout_uri - puts "logout_uri: #{session[:id_token]}" - puts "logout_uri: #{session[:id_token]}" params = { post_logout_redirect_uri: GovOneAuthService::CALLBACKS[:logout], id_token_hint: session[:id_token], diff --git a/spec/system/registered_user/changing_password_spec.rb b/spec/system/registered_user/changing_password_spec.rb index e1e58782c..fec092658 100644 --- a/spec/system/registered_user/changing_password_spec.rb +++ b/spec/system/registered_user/changing_password_spec.rb @@ -5,7 +5,6 @@ let(:password) { 'Str0ngPa$$w0rd' } - include_context 'with user' before do @@ -32,8 +31,7 @@ click_button 'Save' expect(page).to have_current_path '/my-account' expect(page).to have_text('Manage your account') # page heading - .and have_text('Your new password has been saved.') # flash message - .and have_text("Password last changed on #{today}") # event + .and have_text('Your new password has been saved.') end end From 3eb13d5623bb4590cd6c8318a7b778510eb645df Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 6 Dec 2023 11:04:34 +0000 Subject: [PATCH 097/122] dry up auth service spec --- spec/services/gov_one_auth_service_spec.rb | 29 ++++++++++------------ 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/spec/services/gov_one_auth_service_spec.rb b/spec/services/gov_one_auth_service_spec.rb index 66b65187b..94e08d9ce 100644 --- a/spec/services/gov_one_auth_service_spec.rb +++ b/spec/services/gov_one_auth_service_spec.rb @@ -1,5 +1,12 @@ require 'rails_helper' +shared_examples 'a one login request' do + it 'returns a hash of the expected payload' do + expect(result).to eq(payload) + expect(auth_service).to have_received(:response).with(an_instance_of(request_type), an_instance_of(Net::HTTP)) + end +end + RSpec.describe GovOneAuthService do let(:code) { 'mock_code' } let(:mock_response) { instance_double('response') } @@ -13,42 +20,32 @@ describe '#tokens' do let(:result) { auth_service.tokens } + let(:request_type) { Net::HTTP::Post } context 'when successful' do - it 'returns a hash of tokens' do - expect(result).to eq(payload) - expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Post), an_instance_of(Net::HTTP)) - end + it_behaves_like 'a one login request' end context 'when unsuccessful' do let(:payload) { {} } - it 'returns an empty hash' do - expect(result).to eq(payload) - expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Post), an_instance_of(Net::HTTP)) - end + it_behaves_like 'a one login request' end end describe '#user_info' do let(:access_token) { 'mock_access_token' } let(:result) { auth_service.user_info(access_token) } + let(:request_type) { Net::HTTP::Get } context 'when successful' do - it 'returns a hash of user info' do - expect(result).to eq(payload) - expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Get), an_instance_of(Net::HTTP)) - end + it_behaves_like 'a one login request' end context 'when unsuccessful' do let(:payload) { {} } - it 'returns an empty hash' do - expect(result).to eq(payload) - expect(auth_service).to have_received(:response).with(an_instance_of(Net::HTTP::Get), an_instance_of(Net::HTTP)) - end + it_behaves_like 'a one login request' end end From a59d6c48c0941d37d4b7674cc397c51403adafdc Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 6 Dec 2023 11:28:47 +0000 Subject: [PATCH 098/122] update seed_snippets_spec locales count --- spec/lib/seed_snippets_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index fbcf787bf..012f85abc 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 194 + expect(locales.count).to be 199 end it 'dot separated key -> Page::Resource#name' do From 39d565cf3432cca7c1611abb8e8b93f76dbb5c24 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 6 Dec 2023 11:33:02 +0000 Subject: [PATCH 099/122] fix merge conflicts --- .github/workflows/ci.yml | 1 + Gemfile.lock | 69 ---------------------------------- spec/lib/seed_snippets_spec.rb | 4 -- 3 files changed, 1 insertion(+), 73 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afcda98a2..b0b10efcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: DATABASE_URL: postgres://postgres:password@localhost:5432/test DOMAIN: recovery.app BOT_TOKEN: bot_token + services: postgres: image: postgres:15.4-alpine diff --git a/Gemfile.lock b/Gemfile.lock index 432493a7b..661ce8319 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,17 +68,11 @@ GEM tzinfo (~> 2.0) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) -<<<<<<< HEAD - aes_key_wrap (1.1.0) - ahoy_matey (5.0.1) -======= ahoy_matey (5.0.2) ->>>>>>> origin activesupport (>= 6.1) device_detector (>= 1) safely_block (>= 0.4) ast (2.4.2) - attr_required (1.0.1) backports (3.24.1) base64 (0.1.1) bcrypt (3.1.19) @@ -86,7 +80,6 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bindata (2.4.15) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -192,8 +185,6 @@ GEM base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-follow_redirects (0.3.0) - faraday (>= 1, < 3) faraday-net_http (3.0.2) ffi (1.16.3) ffi-compiler (1.0.1) @@ -286,12 +277,6 @@ GEM jsbundling-rails (1.2.1) railties (>= 6.0.0) json (2.6.3) - json-jwt (1.16.3) - activesupport (>= 4.2) - aes_key_wrap - bindata - faraday (~> 2.0) - faraday-follow_redirects jwt (2.7.1) language_server-protocol (3.17.0.3) launchy (2.5.2) @@ -335,29 +320,6 @@ GEM racc (~> 1.4) notifications-ruby-client (5.4.0) jwt (>= 1.5, < 3) - omniauth (2.1.1) - hashie (>= 3.4.6) - rack (>= 2.2.3) - rack-protection - omniauth-rails_csrf_protection (1.0.1) - actionpack (>= 4.2) - omniauth (~> 2.0) - omniauth_openid_connect (0.7.1) - omniauth (>= 1.9, < 3) - openid_connect (~> 2.2) - openid_connect (2.2.0) - activemodel - attr_required (>= 1.0.0) - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.16) - net-smtp - rack-oauth2 (~> 2.2) - swd (~> 2.0) - tzinfo - validate_email - validate_url - webfinger (~> 2.0) orm_adapter (0.5.0) os (1.1.4) pagy (6.2.0) @@ -392,15 +354,6 @@ GEM raabro (1.4.0) racc (1.7.3) rack (2.2.8) - rack-oauth2 (2.2.0) - activesupport - attr_required - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.11.0) - rack (>= 2.1.0) - rack-protection (3.1.0) - rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) rails (7.0.8) @@ -560,11 +513,6 @@ GEM stimulus-rails (1.2.2) railties (>= 6.0.0) stringio (3.0.8) - swd (2.0.2) - activesupport (>= 3) - attr_required (>= 0.0.5) - faraday (~> 2.0) - faraday-follow_redirects temple (0.10.3) thor (1.3.0) tilt (2.3.0) @@ -581,17 +529,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.5.0) -<<<<<<< HEAD - validate_email (0.1.6) - activemodel (>= 3.0) - mail (>= 2.2.5) - validate_url (1.0.15) - activemodel (>= 3.0.0) - public_suffix - view_component (3.5.0) -======= view_component (3.6.0) ->>>>>>> origin activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) @@ -602,10 +540,6 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webfinger (2.1.2) - activesupport - faraday (~> 2.0) - faraday-follow_redirects webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -664,10 +598,7 @@ DEPENDENCIES importmap-rails jbuilder jsbundling-rails - jwt launchy - omniauth-rails_csrf_protection - omniauth_openid_connect pg pry-byebug pry-doc diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 77e22c09a..012f85abc 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,11 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do -<<<<<<< HEAD expect(locales.count).to be 199 -======= - expect(locales.count).to be 189 ->>>>>>> origin end it 'dot separated key -> Page::Resource#name' do From 239761219e5525823c0c106fa668fd349f7d9b22 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 7 Dec 2023 16:26:22 +0000 Subject: [PATCH 100/122] udpate Gemfile.lock --- Gemfile.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 661ce8319..fe7a62830 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,11 +68,13 @@ GEM tzinfo (~> 2.0) addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) + aes_key_wrap (1.1.0) ahoy_matey (5.0.2) activesupport (>= 6.1) device_detector (>= 1) safely_block (>= 0.4) ast (2.4.2) + attr_required (1.0.1) backports (3.24.1) base64 (0.1.1) bcrypt (3.1.19) @@ -80,6 +82,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) + bindata (2.4.15) bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) @@ -185,6 +188,8 @@ GEM base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) faraday-net_http (3.0.2) ffi (1.16.3) ffi-compiler (1.0.1) @@ -277,6 +282,12 @@ GEM jsbundling-rails (1.2.1) railties (>= 6.0.0) json (2.6.3) + json-jwt (1.16.3) + activesupport (>= 4.2) + aes_key_wrap + bindata + faraday (~> 2.0) + faraday-follow_redirects jwt (2.7.1) language_server-protocol (3.17.0.3) launchy (2.5.2) @@ -320,6 +331,29 @@ GEM racc (~> 1.4) notifications-ruby-client (5.4.0) jwt (>= 1.5, < 3) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + omniauth_openid_connect (0.7.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 2.2) + openid_connect (2.2.0) + activemodel + attr_required (>= 1.0.0) + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.16) + net-smtp + rack-oauth2 (~> 2.2) + swd (~> 2.0) + tzinfo + validate_email + validate_url + webfinger (~> 2.0) orm_adapter (0.5.0) os (1.1.4) pagy (6.2.0) @@ -354,6 +388,15 @@ GEM raabro (1.4.0) racc (1.7.3) rack (2.2.8) + rack-oauth2 (2.2.0) + activesupport + attr_required + faraday (~> 2.0) + faraday-follow_redirects + json-jwt (>= 1.11.0) + rack (>= 2.1.0) + rack-protection (3.1.0) + rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) rails (7.0.8) @@ -513,6 +556,11 @@ GEM stimulus-rails (1.2.2) railties (>= 6.0.0) stringio (3.0.8) + swd (2.0.2) + activesupport (>= 3) + attr_required (>= 0.0.5) + faraday (~> 2.0) + faraday-follow_redirects temple (0.10.3) thor (1.3.0) tilt (2.3.0) @@ -529,6 +577,12 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.5.0) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) + validate_url (1.0.15) + activemodel (>= 3.0.0) + public_suffix view_component (3.6.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) @@ -540,6 +594,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webfinger (2.1.2) + activesupport + faraday (~> 2.0) + faraday-follow_redirects webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -598,7 +656,10 @@ DEPENDENCIES importmap-rails jbuilder jsbundling-rails + jwt launchy + omniauth-rails_csrf_protection + omniauth_openid_connect pg pry-byebug pry-doc From ae583a675301e8c5d151372fabbca64986b89e1e Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 7 Dec 2023 16:34:39 +0000 Subject: [PATCH 101/122] update seeds_snippets_spec locales --- spec/lib/seed_snippets_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 012f85abc..80ef91229 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 199 + expect(locales.count).to be 201 end it 'dot separated key -> Page::Resource#name' do From e19270b93e22fe709fa61a4cf59ee71c31503ff2 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 7 Dec 2023 16:46:00 +0000 Subject: [PATCH 102/122] remove print to console --- spec/system/registered_user/changing_password_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/system/registered_user/changing_password_spec.rb b/spec/system/registered_user/changing_password_spec.rb index fec092658..954654d8e 100644 --- a/spec/system/registered_user/changing_password_spec.rb +++ b/spec/system/registered_user/changing_password_spec.rb @@ -8,7 +8,6 @@ include_context 'with user' before do - puts user.email visit '/my-account/edit-password' fill_in 'Enter your current password', with: 'Str0ngPa$$w0rd' fill_in 'Create a new password', with: password From 7d7f435c4a07150b35f6bfba275cd7fc1e9870c1 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 8 Dec 2023 10:45:49 +0000 Subject: [PATCH 103/122] add request spec for terms and conditions --- .../registration/terms_and_conditions_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 spec/requests/registration/terms_and_conditions_spec.rb diff --git a/spec/requests/registration/terms_and_conditions_spec.rb b/spec/requests/registration/terms_and_conditions_spec.rb new file mode 100644 index 000000000..ba7c49cbc --- /dev/null +++ b/spec/requests/registration/terms_and_conditions_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe 'Registration names', type: :request do + subject(:user) { create(:user, :confirmed) } + + before do + sign_in user + end + + describe 'GET /registration/terms_and_conditions/edit' do + it 'returns http success' do + get edit_registration_name_path + expect(response).to have_http_status(:success) + end + end +end From d461b513be05f3e43418c1f81db7e58bc87dc33a Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 11 Dec 2023 11:52:52 +0000 Subject: [PATCH 104/122] update account page to use feature flag and add system spec --- app/assets/stylesheets/application.scss | 4 +++ app/views/user/show.html.slim | 17 ++++++++++-- spec/system/account_page_spec.rb | 36 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 spec/system/account_page_spec.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0140ccbc2..d7937cd2f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -186,3 +186,7 @@ ul>li>ul>li { #available h2 .govuk-tag { margin-left: govuk-spacing(1); } + +.text-secondary { + color: $govuk-secondary-text-colour; +} diff --git a/app/views/user/show.html.slim b/app/views/user/show.html.slim index 39f273dcc..a30d69052 100644 --- a/app/views/user/show.html.slim +++ b/app/views/user/show.html.slim @@ -14,8 +14,21 @@ - row.with_key { 'Name' } - row.with_value(text: current_user.name, classes: %w[data-hj-suppress]) - row.with_action(text: 'Change name', href: edit_registration_name_path, html_attributes: { id: :edit_name_registration }) - p.text-secondary - = t('my_account.name_information') + + - unless Rails.application.gov_one_login? + - your_details.with_row do |row| + - row.with_key { 'Email' } + - row.with_value(text: current_user.email, classes: %w[data-hj-suppress]) + - row.with_action(text: 'Change email', href: edit_email_user_path, html_attributes: { id: :edit_email_user }) + - your_details.with_row do |row| + - row.with_key { 'Password' } + - row.with_value { t('my_account.password_changed', date: current_user.password_last_changed) } + - row.with_action(text: 'Change password', href: edit_password_user_path, html_attributes: { id: :edit_password_user }) + + - if Rails.application.gov_one_login? + p.text-secondary + = t('my_account.name_information') + = govuk_summary_list do |other_details| - other_details.with_row do |row| diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb new file mode 100644 index 000000000..ec36ad24e --- /dev/null +++ b/spec/system/account_page_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe 'Account page', type: :system do + subject(:user) { create :user, :registered } + + include_context 'with user' + + context 'when gov_one login is disabled' do + before do + allow(Rails.application).to receive(:gov_one_login?).and_return(false) + visit '/my-account' + end + + it 'displays account details and password options' do + expect(page).to have_text('Manage your account') + expect(page).to have_css('a', text: 'Change name') + expect(page).to have_css('a', text: 'Change password') + expect(page).to have_css('a', text: 'Change setting details') + expect(page).to have_css('a', text: 'Change email preferences') + expect(page).to have_text('Closing your account') + end + end + + context 'when gov_one login is enabled' do + before do + allow(Rails.application).to receive(:gov_one_login?).and_return(true) + visit '/my-account' + end + + it 'password options are not listed and helper text is displayed' do + expect(page).to have_text('Manage your account') + expect(page).not_to have_css('a', text: 'Change password') + expect(page).to have_content('Changing your name on this account will not affect your Gov.UK One Login account') + end + end +end From 776d8d5f5b23cf3d3f6f3524b35517794cfecbe4 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 11 Dec 2023 12:01:06 +0000 Subject: [PATCH 105/122] update locales content --- config/locales/en.yml | 2 +- spec/system/account_page_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 9583ad135..0964023ae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -299,7 +299,7 @@ en: # /my-account my_account: title: My account - name_information: This is the name that will appear on you end of module certificate. You can use this setting to change how your name appears. Changing your name on this account will not affect your GOV.UK One Login account. + name_information: This is the name that will appear on you end of module certificate. You can use this setting to change how your name appears. Changing your name on this account will not affect your GOV.UK One Login setting_details: | ## Your setting details email_preferences: | diff --git a/spec/system/account_page_spec.rb b/spec/system/account_page_spec.rb index ec36ad24e..ac3a0547d 100644 --- a/spec/system/account_page_spec.rb +++ b/spec/system/account_page_spec.rb @@ -30,7 +30,7 @@ it 'password options are not listed and helper text is displayed' do expect(page).to have_text('Manage your account') expect(page).not_to have_css('a', text: 'Change password') - expect(page).to have_content('Changing your name on this account will not affect your Gov.UK One Login account') + expect(page).to have_content('Changing your name on this account will not affect your Gov.UK One Login') end end end From 76a6e62d574735ebb4f31116a7999a7d08d6208c Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 15 Dec 2023 16:14:06 +0000 Subject: [PATCH 106/122] refactor id_token verification and update specs --- .../users/omniauth_callbacks_controller.rb | 14 ++++++++++++-- app/services/gov_one_auth_service.rb | 9 --------- .../users/omniauth_callbacks_controller_spec.rb | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 405db0772..ad37e3f73 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -5,16 +5,17 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # This method is called by Devise after successful Gov One Login authentication # @return [nil] def openid_connect - return unless session_params? && valid_params? + return error_redirect unless session_params? && valid_params? auth_service = GovOneAuthService.new(code: params['code']) tokens_response = auth_service.tokens return error_redirect unless valid_tokens?(tokens_response) id_token = auth_service.decode_id_token(tokens_response['id_token'])[0] + return error_redirect unless valid_id_token?(id_token) + session[:id_token] = tokens_response['id_token'] gov_one_id = id_token['sub'] - return error_redirect unless auth_service.valid_id_token?(id_token, session[:gov_one_auth_nonce]) user_info_response = auth_service.user_info(tokens_response['access_token']) email = user_info_response['email'] @@ -45,6 +46,15 @@ def valid_tokens?(tokens_response) tokens_response.present? && tokens_response['access_token'].present? && tokens_response['id_token'].present? end + # @param id_token [Hash] + # @return [Boolean] + def valid_id_token?(id_token) + id_token.present? && + id_token['nonce'] == session[:gov_one_auth_nonce] && + id_token['iss'] == ENV['GOV_ONE_ISSUER'] && + id_token['aud'] == ENV['GOV_ONE_CLIENT_ID'] + end + # @param user_info_response [Hash] # @return [Boolean] def valid_user_info?(user_info_response, gov_one_id) diff --git a/app/services/gov_one_auth_service.rb b/app/services/gov_one_auth_service.rb index c212865ec..0c36dab39 100644 --- a/app/services/gov_one_auth_service.rb +++ b/app/services/gov_one_auth_service.rb @@ -38,15 +38,6 @@ def tokens {} end - # @param token [String] - # @param nonce [String] - # @return [Boolean] - def valid_id_token?(token, nonce) - token['iss'] == "#{Rails.application.config.gov_one_base_uri}/" && - token['aud'] == Rails.application.config.gov_one_client_id && - token['nonce'] == nonce - end - # GET /userinfo # @param access_token [String] # @return [Hash] diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index fd6c2b331..f42b04a05 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -4,7 +4,7 @@ let(:auth_service) { instance_double(GovOneAuthService) } let(:access_token) { 'mock_access_token' } let(:id_token) { 'mock_id_token' } - let(:decoded_id_token) { { 'sub' => 'mock_sub' } } + let(:decoded_id_token) { { 'sub' => 'mock_sub', 'nonce' => 'mock_nonce', 'iss' => ENV['GOV_ONE_ISSUER'], 'aud' => ENV['GOV_ONE_CLIENT_ID'] } } let(:email) { 'test@example.com' } let(:params) do { 'code' => 'mock_code', 'state' => 'mock_state' } @@ -20,7 +20,6 @@ allow(auth_service).to receive(:user_info).and_return({ 'email' => email, 'sub' => 'mock_sub' }) allow(auth_service).to receive(:jwt_assertion).and_return('mock_jwt_assertion') allow(auth_service).to receive(:decode_id_token).and_return([decoded_id_token]) - allow(auth_service).to receive(:valid_id_token?).and_return(true) end context 'with a new user' do @@ -66,4 +65,16 @@ expect(response).to redirect_to my_modules_path end end + + context 'with invalid session parameters' do + before do + session[:gov_one_auth_state] = nil + get :openid_connect, params: params + end + + it 'redirects to root path with an error message' do + expect(flash[:alert]).to eq 'There was a problem signing in. Please try again.' + expect(response).to redirect_to root_path + end + end end From 49a5799d097f4468fe5cd8887045d78452b190ef Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Fri, 15 Dec 2023 16:26:03 +0000 Subject: [PATCH 107/122] rubocop --- spec/controllers/users/omniauth_callbacks_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index f42b04a05..c9863c1dc 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -71,7 +71,7 @@ session[:gov_one_auth_state] = nil get :openid_connect, params: params end - + it 'redirects to root path with an error message' do expect(flash[:alert]).to eq 'There was a problem signing in. Please try again.' expect(response).to redirect_to root_path From ea0210ddaf3d5e34a4b0475407d996fbdbd38faf Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 18 Dec 2023 10:25:58 +0000 Subject: [PATCH 108/122] update id token verification --- app/controllers/users/omniauth_callbacks_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index ad37e3f73..1b88c307d 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -51,8 +51,8 @@ def valid_tokens?(tokens_response) def valid_id_token?(id_token) id_token.present? && id_token['nonce'] == session[:gov_one_auth_nonce] && - id_token['iss'] == ENV['GOV_ONE_ISSUER'] && - id_token['aud'] == ENV['GOV_ONE_CLIENT_ID'] + id_token['iss'] == Rails.application.config.gov_one_base_uri + "/" && + id_token['aud'] == Rails.application.config.gov_one_client_id end # @param user_info_response [Hash] From 7154861d51c9c0ce6a8f38eb48f53895f883689d Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 18 Dec 2023 10:36:34 +0000 Subject: [PATCH 109/122] rubocop --- app/controllers/users/omniauth_callbacks_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 1b88c307d..a39506be8 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -51,7 +51,7 @@ def valid_tokens?(tokens_response) def valid_id_token?(id_token) id_token.present? && id_token['nonce'] == session[:gov_one_auth_nonce] && - id_token['iss'] == Rails.application.config.gov_one_base_uri + "/" && + id_token['iss'] == "#{Rails.application.config.gov_one_base_uri}/" && id_token['aud'] == Rails.application.config.gov_one_client_id end From d10cee518c9567d82450f76b787dbff077b9c53d Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 18 Dec 2023 11:31:36 +0000 Subject: [PATCH 110/122] update spec --- spec/controllers/users/omniauth_callbacks_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/users/omniauth_callbacks_controller_spec.rb b/spec/controllers/users/omniauth_callbacks_controller_spec.rb index c9863c1dc..47eb35f58 100644 --- a/spec/controllers/users/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/users/omniauth_callbacks_controller_spec.rb @@ -4,7 +4,7 @@ let(:auth_service) { instance_double(GovOneAuthService) } let(:access_token) { 'mock_access_token' } let(:id_token) { 'mock_id_token' } - let(:decoded_id_token) { { 'sub' => 'mock_sub', 'nonce' => 'mock_nonce', 'iss' => ENV['GOV_ONE_ISSUER'], 'aud' => ENV['GOV_ONE_CLIENT_ID'] } } + let(:decoded_id_token) { { 'sub' => 'mock_sub', 'nonce' => 'mock_nonce', 'iss' => "#{Rails.application.config.gov_one_base_uri}/", 'aud' => Rails.application.config.gov_one_client_id } } let(:email) { 'test@example.com' } let(:params) do { 'code' => 'mock_code', 'state' => 'mock_state' } From ba70feee924a369d3cb522fc5610e775f6ebbf64 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 18 Dec 2023 15:11:31 +0000 Subject: [PATCH 111/122] add feature flag to homepage changes --- app/assets/stylesheets/application.scss | 12 ++++ app/views/home/index.html.slim | 77 ++++++++++++++++++------- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d7937cd2f..a614a139e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -135,6 +135,14 @@ pre.code-tip { margin-bottom: govuk-spacing(6); } + &.enrol-box { + border-radius: 6px; + + .govuk-button { + margin: 0; + } + } + } .float-right { @@ -183,6 +191,10 @@ ul>li>ul>li { display: none; } +.white-space-pre-wrap { + white-space: pre-wrap; +} + #available h2 .govuk-tag { margin-left: govuk-spacing(1); } diff --git a/app/views/home/index.html.slim b/app/views/home/index.html.slim index 3cc7f73e2..e8ebba061 100644 --- a/app/views/home/index.html.slim +++ b/app/views/home/index.html.slim @@ -1,27 +1,60 @@ -- content_for :page_title do - = html_title t('home.title') +- if Rails.application.gov_one_login? + - content_for :page_title do + = html_title t('home.title') -- content_for :hero do - = render 'hero' + - content_for :hero do + = render 'hero' -= render 'learning/cms_debug' -= render 'debug' + = render 'learning/cms_debug' + = render 'debug' -.govuk-grid-row - .govuk-grid-column-one-half - = m('home.about', headings_start_with: 'xl') + .govuk-grid-row + .govuk-grid-column-one-half + = m('home.about', headings_start_with: 'xl') + + .prompt.prompt-home + .govuk-grid-row + .govuk-grid-column-one-quarter + i.fa-2x.fa-solid.fa-circle-info aria-describedby='info icon' + + .govuk-grid-column-three-quarters + = m('home.prompt', headings_start_with: 'xl') + + - unless current_user + .govuk-grid-row class="govuk-!-margin-top-9" + .govuk-grid-column-full + = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do + | #{t('home.gov_one_button')} + = render 'chevron' + +- else + - content_for :page_title do + = html_title t('home.title') + + - content_for :hero do + = render 'hero' + + = render 'learning/cms_debug' + = render 'debug' -.prompt.prompt-home .govuk-grid-row - .govuk-grid-column-one-quarter - i.fa-2x.fa-solid.fa-circle-info aria-describedby='info icon' - - .govuk-grid-column-three-quarters - = m('home.prompt', headings_start_with: 'xl') - -- unless current_user - .govuk-grid-row class="govuk-!-margin-top-9" - .govuk-grid-column-full - = govuk_button_link_to gov_one_info_path, class: "govuk-button--start" do - | #{t('home.gov_one_button')} - = render 'chevron' + .govuk-grid-column-one-half + = m('home.about', headings_start_with: 'xl') + + - unless current_user + .govuk-grid-column-one-half + .light-grey-box.enrol-box + = m('home.login', headings_start_with: 'xl') + + .govuk-button-group + = govuk_button_link_to 'Sign in', new_user_session_path + .white-space-pre-wrap= ' or ' + = govuk_link_to 'create an account', new_user_registration_path + + .prompt.prompt-home + .govuk-grid-row + .govuk-grid-column-one-quarter + i.fa-2x.fa-solid.fa-circle-info aria-describedby='info icon' + + .govuk-grid-column-three-quarters + = m('home.prompt', headings_start_with: 'xl') From 6b7bcc4139939a722c56e1048b379f462137671a Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 18 Dec 2023 15:21:44 +0000 Subject: [PATCH 112/122] revert changes to front_page_spec --- spec/system/front_page_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/system/front_page_spec.rb b/spec/system/front_page_spec.rb index 9216e8620..131f4c5b3 100644 --- a/spec/system/front_page_spec.rb +++ b/spec/system/front_page_spec.rb @@ -7,16 +7,16 @@ it 'logged in content' do visit '/' expect(page).to have_text 'Learn more' - expect(page).not_to have_text 'Learn more about this training' - expect(page).not_to have_text 'Start your training now' + expect(page).not_to have_text 'Learn more and enrol' + expect(page).not_to have_text 'Sign in to continue learning' end end context 'with an unauthenticated visitor' do it 'log in content' do visit '/' - expect(page).to have_text('Learn more about this training') - .and have_text('Start your training now') + expect(page).to have_text('Learn more and enrol') + .and have_text('Sign in to continue learning') end end -end +end \ No newline at end of file From 273096c590ab14072efd44b061ed8b7ed4fcb24d Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Mon, 18 Dec 2023 15:51:03 +0000 Subject: [PATCH 113/122] add feature flag to homepage hero --- app/views/home/_hero.html.slim | 22 ++++++++++++++++------ spec/system/front_page_spec.rb | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/views/home/_hero.html.slim b/app/views/home/_hero.html.slim index a148caac1..a67002d9c 100644 --- a/app/views/home/_hero.html.slim +++ b/app/views/home/_hero.html.slim @@ -7,12 +7,22 @@ p.govuk-body-l = t('home.hero') - = link_to course_overview_path, class: 'govuk-!-margin-bottom-4 govuk-link--no-visited-state govuk-!-font-weight-bold govuk-body' do - - if current_user - | Learn more - - else - | Learn more about this training - p.govuk-visually-hidden on the course + - if Rails.application.gov_one_login? + = link_to course_overview_path, class: 'govuk-!-margin-bottom-4 govuk-link--no-visited-state govuk-!-font-weight-bold govuk-body' do + - if current_user + | Learn more + - else + | Learn more about this training + p.govuk-visually-hidden on the course + - else + = govuk_button_link_to course_overview_path, class: 'govuk-button--start govuk-!-margin-bottom-4' do + - if current_user + | Learn more + - else + | Learn more and enrol + p.govuk-visually-hidden on the course + + = render 'chevron' .govuk-grid-column-one-third class='govuk-!-text-align-right' = m('home.thumb') \ No newline at end of file diff --git a/spec/system/front_page_spec.rb b/spec/system/front_page_spec.rb index 131f4c5b3..b72275cc3 100644 --- a/spec/system/front_page_spec.rb +++ b/spec/system/front_page_spec.rb @@ -19,4 +19,4 @@ .and have_text('Sign in to continue learning') end end -end \ No newline at end of file +end From 143491ee615d17531f88be35c67b9fde3b9c2ede Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Tue, 19 Dec 2023 16:40:11 +0000 Subject: [PATCH 114/122] add error checks for gov one login auth responses --- .../users/omniauth_callbacks_controller.rb | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index a39506be8..e74c107c4 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -5,6 +5,11 @@ class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController # This method is called by Devise after successful Gov One Login authentication # @return [nil] def openid_connect + if params['error'].present? + Rails.logger.error("Authentication error: #{params['error']}, #{params['error_description']}") + return error_redirect + end + return error_redirect unless session_params? && valid_params? auth_service = GovOneAuthService.new(code: params['code']) @@ -34,7 +39,6 @@ def valid_params? params['code'].present? && params['state'].present? && params['state'] == session[:gov_one_auth_state] end - # check state and nonce are saved in session # @return [Boolean] def session_params? session[:gov_one_auth_state].present? && session[:gov_one_auth_nonce].present? @@ -43,7 +47,10 @@ def session_params? # @param tokens_response [Hash] # @return [Boolean] def valid_tokens?(tokens_response) - tokens_response.present? && tokens_response['access_token'].present? && tokens_response['id_token'].present? + tokens_response.present? && + tokens_response['access_token'].present? && + tokens_response['id_token'].present? && + tokens_response['error'].blank? end # @param id_token [Hash] @@ -58,7 +65,10 @@ def valid_id_token?(id_token) # @param user_info_response [Hash] # @return [Boolean] def valid_user_info?(user_info_response, gov_one_id) - user_info_response.present? && user_info_response['email'].present? && user_info_response['sub'] == gov_one_id + user_info_response.present? && + user_info_response['email'].present? && + user_info_response['sub'] == gov_one_id && + user_info_response['error'].blank? end # @return [nil] From c49d448979b69ea6e1d3b4240eb9fbcffee6be31 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 20 Dec 2023 13:52:07 +0000 Subject: [PATCH 115/122] add markdown file for gov one adr and tech checklist --- gov_one_login/GOV-ONE-LOGIN.md | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 gov_one_login/GOV-ONE-LOGIN.md diff --git a/gov_one_login/GOV-ONE-LOGIN.md b/gov_one_login/GOV-ONE-LOGIN.md new file mode 100644 index 000000000..00bad6797 --- /dev/null +++ b/gov_one_login/GOV-ONE-LOGIN.md @@ -0,0 +1,57 @@ +# GOV.UK One Login + +## ADR +### Context +The integration of GOV.UK One Login user authentication is a requirement of the service going live. This single sign on will allow users to login to the service using their GOV.UK One Login account. + +### Decision +We have decided to use the `omniauth_openid_connect` gem. This decision was based on the fact that `omniauth_openid_connect` is an off-the-shelf OIDC library as was recommended by GOV.UK One Login, it integrates well with Devise (which we are already using for user authentication) and it has a good level of community support. Auth URIs and client secrets are stored in Rails credentials files which is a secure way of storing sensitive information and allows us to use different credentials for different environments. + +### Consequences +By using the `omniauth_openid_connect` gem we have been able to implement the GOV.UK One Login user authentication in a way that is consistent with the rest of the application and has allowed us to use the existing Devise functionality that we have already implemented for user authentication. + + +## Checklist + +### Authentication request requirements +| Requirement | Response | +| --- | --- | +| __Describe how you’re using the state parameter to prevent CSRF attacks__ | The state is generated as a random uuid, which is then stored in the Rails session storage. The state parameter for authorisation request responses must match the state stored in the session before the user is authenticated. +| __Describe how you’re generating the nonce parameter__ | The nonce is a randomly generated alphanumeric string of 25 characters, it is used to verify the `id_token`. | +| __Describe how you handle authorise endpoint errors__ | The errors are logged and the user is redirected to the homepage with an alert informing them of a problem | +| __Describe how you’re handling access_denied errors where session state is also missing__ | The user is redirected to the homepage with an alert informing them of a problem and encouraging them to try again | + +### Token request requirements +| Requirement | Response | +| --- | --- | +| __Describe how you ensure that your client secret / private key is not exposed to unauthorised parties__ | These are encrypted and stored in Rails credentials | +| __For the private_key_jwt confirm that each jti claim value in the JWT assertion is used once.__ | <input type="checkbox" checked> | + +### Token validation requirements +| Requirement | Response | +| --- | --- | +| __Confirm you validate the iss claim is https://oidc.account.gov.uk/__ | <input type="checkbox" checked> | +| __Confirm you validate the aud claim matches your client_id__ | <input type="checkbox" checked> | +| __Confirm you validate the nonce claim matches the your application generated__ | <input type="checkbox" checked> | +| __Confirm you validate the current time is before the time in the exp claim__ | <input type="checkbox" checked> | +| __Confirm you validate the current time is between the time in the auth_time claim and the exp claim__ | <input type="checkbox" checked> | +| __Confirm you validate the signature on the id-token__ | <input type="checkbox" checked> | +| __Describe how you handle token endpoint errors__ | The error is logged and the user is redirected to the homepage with an alert informing them of a problem | +| __Describe how you ensure that the GOV.UK One Login Access Token is not exposed to unauthorised parties outside of your trusted backend server__ | The access token is not exposed to the user and is only used to make requests to the UserInfo endpoint during the user session. Communication with GOV.UK One Login is over HTTPS. | + +### UserInfo request requirements +| Requirement | Response | +| --- | --- | +| __Confirm you validate the sub claim in the UserInfo response matches the id-token sub claim__ | <input type="checkbox" checked> | +| __Describe how you handle UserInfo endpoint errors__ | The error is logged and the user is redirected to the homepage with an alert informing them of a problem | +| __If you’re using the email address scope, confirm that you’re aware this represents the GOV.UK One Login username and may not be the user’s preferred contact email address__ | <input type="checkbox" checked> | + +### Key management requirements +| Requirement | Response | +| --- | --- | +| __If using the GOV.UK One Login OpenID Provider JWKS Endpoint for signature validation describe your approach to key rotation__ | The keys are cached and the cache expires every 24 hours | + +### Session management requirements +| Requirement | Response | +| --- | --- | +| __Confirm that you’ve implemented logout functionality and that your service calls the GOV.UK One Login logout endpoint__ | <input type="checkbox" checked> | From f85fd2b101ae7b916a0ee61643405424b8769004 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 20 Dec 2023 14:04:53 +0000 Subject: [PATCH 116/122] update markdown checkmark for github --- gov_one_login/GOV-ONE-LOGIN.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gov_one_login/GOV-ONE-LOGIN.md b/gov_one_login/GOV-ONE-LOGIN.md index 00bad6797..cfd55f7c9 100644 --- a/gov_one_login/GOV-ONE-LOGIN.md +++ b/gov_one_login/GOV-ONE-LOGIN.md @@ -25,26 +25,26 @@ By using the `omniauth_openid_connect` gem we have been able to implement the GO | Requirement | Response | | --- | --- | | __Describe how you ensure that your client secret / private key is not exposed to unauthorised parties__ | These are encrypted and stored in Rails credentials | -| __For the private_key_jwt confirm that each jti claim value in the JWT assertion is used once.__ | <input type="checkbox" checked> | +| __For the private_key_jwt confirm that each jti claim value in the JWT assertion is used once.__ | [x] | ### Token validation requirements | Requirement | Response | | --- | --- | -| __Confirm you validate the iss claim is https://oidc.account.gov.uk/__ | <input type="checkbox" checked> | -| __Confirm you validate the aud claim matches your client_id__ | <input type="checkbox" checked> | -| __Confirm you validate the nonce claim matches the your application generated__ | <input type="checkbox" checked> | -| __Confirm you validate the current time is before the time in the exp claim__ | <input type="checkbox" checked> | -| __Confirm you validate the current time is between the time in the auth_time claim and the exp claim__ | <input type="checkbox" checked> | -| __Confirm you validate the signature on the id-token__ | <input type="checkbox" checked> | +| __Confirm you validate the iss claim is https://oidc.account.gov.uk/__ | [x] | +| __Confirm you validate the aud claim matches your client_id__ | [x] | +| __Confirm you validate the nonce claim matches the your application generated__ | [x] | +| __Confirm you validate the current time is before the time in the exp claim__ | [x] | +| __Confirm you validate the current time is between the time in the auth_time claim and the exp claim__ | [x] | +| __Confirm you validate the signature on the id-token__ | [x] | | __Describe how you handle token endpoint errors__ | The error is logged and the user is redirected to the homepage with an alert informing them of a problem | | __Describe how you ensure that the GOV.UK One Login Access Token is not exposed to unauthorised parties outside of your trusted backend server__ | The access token is not exposed to the user and is only used to make requests to the UserInfo endpoint during the user session. Communication with GOV.UK One Login is over HTTPS. | ### UserInfo request requirements | Requirement | Response | | --- | --- | -| __Confirm you validate the sub claim in the UserInfo response matches the id-token sub claim__ | <input type="checkbox" checked> | +| __Confirm you validate the sub claim in the UserInfo response matches the id-token sub claim__ | [x] | | __Describe how you handle UserInfo endpoint errors__ | The error is logged and the user is redirected to the homepage with an alert informing them of a problem | -| __If you’re using the email address scope, confirm that you’re aware this represents the GOV.UK One Login username and may not be the user’s preferred contact email address__ | <input type="checkbox" checked> | +| __If you’re using the email address scope, confirm that you’re aware this represents the GOV.UK One Login username and may not be the user’s preferred contact email address__ | [x] | ### Key management requirements | Requirement | Response | @@ -54,4 +54,4 @@ By using the `omniauth_openid_connect` gem we have been able to implement the GO ### Session management requirements | Requirement | Response | | --- | --- | -| __Confirm that you’ve implemented logout functionality and that your service calls the GOV.UK One Login logout endpoint__ | <input type="checkbox" checked> | +| __Confirm that you’ve implemented logout functionality and that your service calls the GOV.UK One Login logout endpoint__ | [x] | From ae48ab92cbad1b3d8b066cb6eddd19c4b51c8969 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 20 Dec 2023 15:38:10 +0000 Subject: [PATCH 117/122] update one login info markdown and add adr --- .yardopts | 1 + adr/0017-gov-one-login.md | 19 +++++++++++++++++ adr/ADR.md | 3 ++- gov_one_login/GOV-ONE-LOGIN.md | 37 ++++++++++++++++------------------ 4 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 adr/0017-gov-one-login.md diff --git a/.yardopts b/.yardopts index 72d6af40f..6f31f194c 100644 --- a/.yardopts +++ b/.yardopts @@ -5,6 +5,7 @@ --markup markdown - cms/*.md +gov_one_login/*.md data/*.md uml/*.md adr/*.md diff --git a/adr/0017-gov-one-login.md b/adr/0017-gov-one-login.md new file mode 100644 index 000000000..951b07c06 --- /dev/null +++ b/adr/0017-gov-one-login.md @@ -0,0 +1,19 @@ +# GOV.UK One Login + +* Status: accepted + +## Context and Problem Statement +The integration of GOV.UK One Login user authentication is a requirement of the service going live. This single sign on will allow users to login to the service using their GOV.UK One Login account. + +## Decision Drivers +* GOV.UK One Login reccomends using an off-the-shelf OIDC library +* We currently use Devise for user authentication +* Omniauth would allow us to use Devise and integrate with GOV.UK One Login + +## Considered Options +* [omniauth](https://github.com/omniauth/omniauth) +* [omniauth_openid_connect](https://github.com/omniauth/omniauth_openid_connect) + +## Decision Outcome +Chosen option: [omniauth_openid_connect](https://github.com/omniauth/omniauth_openid_connect) + diff --git a/adr/ADR.md b/adr/ADR.md index fd3badbf9..2c19dfed9 100644 --- a/adr/ADR.md +++ b/adr/ADR.md @@ -20,7 +20,8 @@ This log lists the architectural decisions for EYFS Recovery * [ADR-0013](0013-sensitive-data-encryption.md) - Use Active Record Encryption to protect sensitive data * [ADR-0014](0014-user-tracking.md) - Use Hotjar for tracking user journeys * [ADR-0015](0015-background-jobs.md) - Use Que for processing background jobs -* [ADR-0016](0016-devise-security-and-pwned-passwords-gems.md) - Use Devise Security and Devise Pwned Password gems +* [ADR-0016](0016-devise-security-and-pwned-password-gems.md) - Use gems Devise Security and Deviese Pwned Passwords +* [ADR-0017](0017-gov-one-login.md) - GOV.UK One Login <!-- adrlogstop --> diff --git a/gov_one_login/GOV-ONE-LOGIN.md b/gov_one_login/GOV-ONE-LOGIN.md index cfd55f7c9..8d8a1e2c7 100644 --- a/gov_one_login/GOV-ONE-LOGIN.md +++ b/gov_one_login/GOV-ONE-LOGIN.md @@ -1,17 +1,14 @@ # GOV.UK One Login -## ADR -### Context -The integration of GOV.UK One Login user authentication is a requirement of the service going live. This single sign on will allow users to login to the service using their GOV.UK One Login account. +## Integration Environment +- __Base URI__: https://oidc.integration.account.gov.uk +- __Redirect URLs__: + - http://localhost:3000/users/auth/openid_connect/callback +- __Post-logout redirect URLs__: + - http://localhost:3000/users/sign_out -### Decision -We have decided to use the `omniauth_openid_connect` gem. This decision was based on the fact that `omniauth_openid_connect` is an off-the-shelf OIDC library as was recommended by GOV.UK One Login, it integrates well with Devise (which we are already using for user authentication) and it has a good level of community support. Auth URIs and client secrets are stored in Rails credentials files which is a secure way of storing sensitive information and allows us to use different credentials for different environments. -### Consequences -By using the `omniauth_openid_connect` gem we have been able to implement the GOV.UK One Login user authentication in a way that is consistent with the rest of the application and has allowed us to use the existing Devise functionality that we have already implemented for user authentication. - - -## Checklist +## Technical Checklist ### Authentication request requirements | Requirement | Response | @@ -25,26 +22,26 @@ By using the `omniauth_openid_connect` gem we have been able to implement the GO | Requirement | Response | | --- | --- | | __Describe how you ensure that your client secret / private key is not exposed to unauthorised parties__ | These are encrypted and stored in Rails credentials | -| __For the private_key_jwt confirm that each jti claim value in the JWT assertion is used once.__ | [x] | +| __For the private_key_jwt confirm that each jti claim value in the JWT assertion is used once.__ | ✓ | ### Token validation requirements | Requirement | Response | | --- | --- | -| __Confirm you validate the iss claim is https://oidc.account.gov.uk/__ | [x] | -| __Confirm you validate the aud claim matches your client_id__ | [x] | -| __Confirm you validate the nonce claim matches the your application generated__ | [x] | -| __Confirm you validate the current time is before the time in the exp claim__ | [x] | -| __Confirm you validate the current time is between the time in the auth_time claim and the exp claim__ | [x] | -| __Confirm you validate the signature on the id-token__ | [x] | +| __Confirm you validate the iss claim is https://oidc.account.gov.uk/__ | ✓ | +| __Confirm you validate the aud claim matches your client_id__ | ✓ | +| __Confirm you validate the nonce claim matches the your application generated__ | ✓ | +| __Confirm you validate the current time is before the time in the exp claim__ | ✓ | +| __Confirm you validate the current time is between the time in the auth_time claim and the exp claim__ | ✓ | +| __Confirm you validate the signature on the id-token__ | ✓ | | __Describe how you handle token endpoint errors__ | The error is logged and the user is redirected to the homepage with an alert informing them of a problem | | __Describe how you ensure that the GOV.UK One Login Access Token is not exposed to unauthorised parties outside of your trusted backend server__ | The access token is not exposed to the user and is only used to make requests to the UserInfo endpoint during the user session. Communication with GOV.UK One Login is over HTTPS. | ### UserInfo request requirements | Requirement | Response | | --- | --- | -| __Confirm you validate the sub claim in the UserInfo response matches the id-token sub claim__ | [x] | +| __Confirm you validate the sub claim in the UserInfo response matches the id-token sub claim__ | ✓ | | __Describe how you handle UserInfo endpoint errors__ | The error is logged and the user is redirected to the homepage with an alert informing them of a problem | -| __If you’re using the email address scope, confirm that you’re aware this represents the GOV.UK One Login username and may not be the user’s preferred contact email address__ | [x] | +| __If you’re using the email address scope, confirm that you’re aware this represents the GOV.UK One Login username and may not be the user’s preferred contact email address__ | ✓ | ### Key management requirements | Requirement | Response | @@ -54,4 +51,4 @@ By using the `omniauth_openid_connect` gem we have been able to implement the GO ### Session management requirements | Requirement | Response | | --- | --- | -| __Confirm that you’ve implemented logout functionality and that your service calls the GOV.UK One Login logout endpoint__ | [x] | +| __Confirm that you’ve implemented logout functionality and that your service calls the GOV.UK One Login logout endpoint__ | ✓ | From e4bc64ab4c532c1f918177403b261602ae9daec5 Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 20 Dec 2023 16:20:00 +0000 Subject: [PATCH 118/122] update sitemap --- config/sitemap.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/sitemap.rb b/config/sitemap.rb index 733b813ea..9641548b6 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -68,6 +68,9 @@ # account add user_path + # GOV.UK one login + add gov_one_info_path + # edit registration/account add edit_email_user_path add edit_password_user_path From 9bd4c17db8db4c18a5ac02c14603efeb537fe85e Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 20 Dec 2023 16:23:30 +0000 Subject: [PATCH 119/122] fix typo --- adr/ADR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adr/ADR.md b/adr/ADR.md index 2c19dfed9..646f0d746 100644 --- a/adr/ADR.md +++ b/adr/ADR.md @@ -20,7 +20,7 @@ This log lists the architectural decisions for EYFS Recovery * [ADR-0013](0013-sensitive-data-encryption.md) - Use Active Record Encryption to protect sensitive data * [ADR-0014](0014-user-tracking.md) - Use Hotjar for tracking user journeys * [ADR-0015](0015-background-jobs.md) - Use Que for processing background jobs -* [ADR-0016](0016-devise-security-and-pwned-password-gems.md) - Use gems Devise Security and Deviese Pwned Passwords +* [ADR-0016](0016-devise-security-and-pwned-password-gems.md) - Use gems Devise Security and Devise Pwned Passwords * [ADR-0017](0017-gov-one-login.md) - GOV.UK One Login <!-- adrlogstop --> From 40f12adb6700d723aa7be0445f5346dd8ac0a41e Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Wed, 20 Dec 2023 16:24:48 +0000 Subject: [PATCH 120/122] revert unwanted adr changes --- adr/ADR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adr/ADR.md b/adr/ADR.md index 646f0d746..f36b4cf60 100644 --- a/adr/ADR.md +++ b/adr/ADR.md @@ -20,7 +20,7 @@ This log lists the architectural decisions for EYFS Recovery * [ADR-0013](0013-sensitive-data-encryption.md) - Use Active Record Encryption to protect sensitive data * [ADR-0014](0014-user-tracking.md) - Use Hotjar for tracking user journeys * [ADR-0015](0015-background-jobs.md) - Use Que for processing background jobs -* [ADR-0016](0016-devise-security-and-pwned-password-gems.md) - Use gems Devise Security and Devise Pwned Passwords +* [ADR-0016](0016-devise-security-and-pwned-passwords-gems.md) - Use Devise Security and Devise Pwned Password gems * [ADR-0017](0017-gov-one-login.md) - GOV.UK One Login <!-- adrlogstop --> From eb412893ac02c031cc66151b862e9094a8907a5d Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 21 Dec 2023 11:30:08 +0000 Subject: [PATCH 121/122] update seed snippets spec locales --- spec/lib/seed_snippets_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 80ef91229..eafcf2af5 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 201 + expect(locales.count).to be 204 end it 'dot separated key -> Page::Resource#name' do From 27fb9b145db68bdd896fd1068d6bc9ea5d5304cc Mon Sep 17 00:00:00 2001 From: "jack.coggin" <jack.coggin@and.digital> Date: Thu, 21 Dec 2023 14:18:55 +0000 Subject: [PATCH 122/122] put domain back to app in docker compose --- docker-compose.dev.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 11001e395..7287b9819 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -19,8 +19,7 @@ services: # app config - DATABASE_URL=postgres://postgres:password@db:5432/early_years_foundation_recovery_development - RAILS_ENV=development - # - DOMAIN=app:3000 - - DOMAIN=localhost:3000 + - DOMAIN=app:3000 - RAILS_SERVE_STATIC_FILES=true volumes: - .:/srv