Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OneLogin flow for Jobseekers #7019

Draft
wants to merge 88 commits into
base: one_login
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
a1fce2d
Add JWT gem
scruti Aug 14, 2024
90f7374
Build GovUK One Login endpoints client
scruti Aug 16, 2024
4fc3316
Move GovUkOneLogin to service module
scruti Aug 19, 2024
a6794fb
Add One Login Id field to Jobseekers DB table
scruti Aug 21, 2024
7273bcd
Setup basic Jobseeker OneLogin sign-in flow
scruti Aug 21, 2024
c84faca
Add Rails master key to CI tests
scruti Aug 22, 2024
0a4ab8a
Add bridge page for sign in via govuk one login
KyleMacPherson Aug 23, 2024
87a5b80
Merge pull request #7025 from DFE-Digital/onelogin-bridge-page
KyleMacPherson Aug 28, 2024
5fed8b9
Redirect existing users to account found page if it's their first log…
KyleMacPherson Aug 27, 2024
2895d7c
Merge pull request #7026 from DFE-Digital/onelogin-found-account
KyleMacPherson Aug 28, 2024
884fa85
Add account found page
KyleMacPherson Aug 28, 2024
21540f5
Merge pull request #7027 from DFE-Digital/onelogin-found-account-page
KyleMacPherson Aug 28, 2024
7cb5f01
Update account page due to switching to gov.uk one login to manage jo…
KyleMacPherson Aug 28, 2024
dbfbea5
Copy changes to the confirm destroy account page
KyleMacPherson Aug 28, 2024
b7c7d74
Merge pull request #7028 from DFE-Digital/onelogin-account-page
KyleMacPherson Aug 29, 2024
3df8012
Add link to "Your account" page which takes users to the one login se…
KyleMacPherson Aug 29, 2024
af862ad
Merge pull request #7029 from DFE-Digital/onelogin-add-link-to-your-a…
KyleMacPherson Aug 29, 2024
bbf5af5
Redirect to quick apply vacancy if user is not logged in when they fi…
KyleMacPherson Aug 29, 2024
fa661bd
Merge pull request #7032 from DFE-Digital/onelogin-redirect-to-previo…
KyleMacPherson Sep 2, 2024
d94e9b2
Add notification banner to new job application page
KyleMacPherson Sep 6, 2024
093f2a4
Merge pull request #7047 from DFE-Digital/onelogin-quick-apply-banner
KyleMacPherson Sep 9, 2024
0fce80a
snyk update
KyleMacPherson Sep 9, 2024
178533e
Merge pull request #7055 from DFE-Digital/onelogin-fix-snyk
KyleMacPherson Sep 9, 2024
b4d9afd
Merge remote-tracking branch 'origin/main' into use-one-login-for-job…
scruti Sep 12, 2024
7f55bcd
Merge branch 'one_login' into use-one-login-for-jobseekers
scruti Sep 12, 2024
bbb24d0
Merge remote-tracking branch 'origin/one_login' into use-one-login-fo…
scruti Sep 16, 2024
b3896d7
Fix linting style violations
scruti Sep 12, 2024
5eb5a9e
Fix OneLogin callback redirection and extend tests
scruti Sep 12, 2024
c646a5c
Fix OneLogin omniauth config breaking Publishers
scruti Sep 16, 2024
71d4842
Merge pull request #7087 from DFE-Digital/fix-omniauth-config-for-all…
scruti Sep 17, 2024
8aa8e72
Merge branch 'one_login' into use-one-login-for-jobseekers
scruti Sep 18, 2024
9ce0af7
Add account not found page
KyleMacPherson Sep 17, 2024
e7b550b
Fix typo on account not found page
KyleMacPherson Sep 17, 2024
1dd4b11
Incorrect changes made to account found page
KyleMacPherson Sep 17, 2024
0f91338
Refactor govuk_one_login_callbacks_controller
KyleMacPherson Sep 18, 2024
015f13b
Add transfer account link to account not found page
KyleMacPherson Sep 18, 2024
b423bc5
Fix email addresses in govuk_one_login_spec
KyleMacPherson Sep 18, 2024
cb940c0
Linting
KyleMacPherson Sep 18, 2024
8e884fa
Fix link on account_not_found page
KyleMacPherson Sep 18, 2024
ccf0381
Fix failing unit tests
KyleMacPherson Sep 19, 2024
9ef870b
Merge pull request #7090 from DFE-Digital/onelogin-not-account-found
KyleMacPherson Sep 19, 2024
b0c6e08
Add notification banner to profiles page letting users know they can …
KyleMacPherson Sep 18, 2024
466bf28
Merge pull request #7097 from DFE-Digital/onelogin-profile-notification
KyleMacPherson Sep 19, 2024
cb543c2
Fix link to transfer account on new job application
KyleMacPherson Sep 20, 2024
0446777
Fix link to transfer account details on your account page
KyleMacPherson Sep 20, 2024
495915a
Remove missing line
KyleMacPherson Sep 20, 2024
13a2d68
Merge pull request #7100 from DFE-Digital/onelogin-fix-links
KyleMacPherson Sep 20, 2024
899e66a
Merge branch 'one_login' into use-one-login-for-jobseekers
scruti Sep 23, 2024
7748cfb
Merge branch 'one_login' into use-one-login-for-jobseekers
scruti Sep 24, 2024
f1fa10e
Merge remote-tracking branch 'origin/one_login' into use-one-login-fo…
scruti Sep 25, 2024
ec5b362
Add system tests for Jobseeker OneLogin flow
scruti Sep 24, 2024
c4b3fa7
Add quick apply sign-in redirection to system tests
scruti Sep 24, 2024
7547c99
Fix: Sign-out publisher when signing as Jobseeker
scruti Sep 24, 2024
410ad2f
Move system tests file now only used for Publisers
scruti Sep 24, 2024
e62dce7
Adapt jobseeker dashboard tests to OneLogin changes
scruti Sep 24, 2024
90b9e8d
Disable Jobseeker Sign-up tests
scruti Sep 24, 2024
788823d
Unify duplicated system specs
scruti Sep 24, 2024
1918e3d
Disable Jobseeker account unlock tests
scruti Sep 24, 2024
52c90e8
Use OneLogin helper method to sign-in system specs
scruti Sep 24, 2024
782528d
Allow other sign-in redirection paths
scruti Sep 25, 2024
c1c5d68
Merge pull request #7072 from DFE-Digital/setup-tests-with-onelogin-j…
scruti Sep 26, 2024
0c6f787
Allow email updates from OneLogin
scruti Sep 27, 2024
789ce41
Merge pull request #7120 from DFE-Digital/allow-jobseekers-to-update-…
scruti Sep 27, 2024
a78a2d3
populate email address if account transfer fails and it re-renders th…
KyleMacPherson Oct 1, 2024
b369d11
Add additional logging for debugging purposes
KyleMacPherson Oct 1, 2024
38ccd57
Merge pull request #7130 from DFE-Digital/onelogin-bug-add-logging
KyleMacPherson Oct 1, 2024
d074af4
Sign-out Jobseekers through Govuk OneLogin
scruti Sep 30, 2024
e862018
Merge pull request #7128 from DFE-Digital/implement-session-logout-th…
scruti Oct 1, 2024
4a3eb20
Revert "Sign-out Jobseekers through Govuk OneLogin"
scruti Oct 3, 2024
e3eb99e
Merge pull request #7140 from DFE-Digital/revert-7128-implement-sessi…
scruti Oct 3, 2024
e7cfb1a
Merge remote-tracking branch 'origin/one_login' into use-one-login-fo…
scruti Oct 4, 2024
5b32cd5
Exclude the one login id from Rails param logging
scruti Oct 4, 2024
ea90cd3
Merge pull request #7144 from DFE-Digital/filter-one-login-id-from-ra…
scruti Oct 4, 2024
13946dd
Remove word "old" from new sign in guidance
KyleMacPherson Oct 4, 2024
1efbdbe
Tweaks to sign in guidance
KyleMacPherson Oct 4, 2024
4937724
Change where the sign in as jobseeker link takes user. Now taking the…
KyleMacPherson Oct 4, 2024
0f70ee2
Fix login prompt after creating a subscription
scruti Oct 4, 2024
74b6365
Remove registration link from forced login banner
scruti Oct 4, 2024
9b015a3
Style: Remove spaces ant end of lines
scruti Oct 4, 2024
2e1ce24
Fix issue causing the jobseekers email to be lost on the account tran…
KyleMacPherson Oct 4, 2024
2e1b4eb
Merge pull request #7143 from DFE-Digital/fix-login-prompt-after-crea…
scruti Oct 4, 2024
f115d43
Merge pull request #7145 from DFE-Digital/onelogin-tweaks-2
KyleMacPherson Oct 7, 2024
d430a33
Add notification to confirm that the code has been successfully resen…
KyleMacPherson Oct 8, 2024
5286ef8
Add banner to ensure confirmation notice is shown at the correct time…
KyleMacPherson Oct 8, 2024
66ccf06
rubocop linting
KyleMacPherson Oct 8, 2024
8723345
Change notice to success
KyleMacPherson Oct 8, 2024
b7f5222
Add success message to i18n files
KyleMacPherson Oct 8, 2024
fd81fab
Merge pull request #7153 from DFE-Digital/onelogin-resend-email-banner
KyleMacPherson Oct 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ DFE_SIGN_IN_SUPPORT_USER_ROLE_ID=test-support-user-role-id
DFE_SIGN_IN_URL=https://test-url.local
DISABLE_EMAILS=false
DOMAIN=localhost:3000
GOVUK_ONE_LOGIN_BASE_URL=https://test-onelogin-url.local
GOVUK_ONE_LOGIN_CLIENT_ID=one_login_client_id
VACANCY_SOURCE_UNITED_LEARNING_FEED_URL=http://example.com/feed.xml
VACANCY_SOURCE_VENTRUS_FEED_URL=http://example.com/feed.xml
VACANCY_SOURCE_FUSION_FEED_URL=http://example.com/feed.json
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ jobs:
with:
build-args: |
BUILDKIT_INLINE_CACHE=1
RAILS_MASTER_KEY=${{ secrets.RAILS_MASTER_KEY }}
# Cache from builder target tagged with branch name, may be empty first time branch is pushed
# Cache from builder target tagged with main branch name, always present, maybe less recent
cache-from: |
Expand All @@ -111,6 +112,7 @@ jobs:
build-args: |
BUILDKIT_INLINE_CACHE=1
COMMIT_SHA=${{ env.COMMIT_SHA }}
RAILS_MASTER_KEY=${{ secrets.RAILS_MASTER_KEY }}
# Cache from builder target built above, always present
# Cache from production target tagged with branch name, may be empty first time branch is pushed
# Cache from production target tagged with main branch name, always present, maybe less recent
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ jobs:
runs-on: ubuntu-20.04

env:
RAILS_ENV: test
DATABASE_URL: postgis://postgres:postgres@localhost:5432/tvs_test
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}

services:
postgres:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ jobs:
params: '--exclude-pattern "spec/{system}/*_spec.rb, spec/system/{jobseekers,publishers,support_users,other}/*_spec.rb"'

env:
RAILS_ENV: test
DATABASE_URL: postgis://postgres:postgres@localhost:5432/tvs_test
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}

services:
postgres:
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
# configuring it using the ENV variables we provide in storage.yml. However, at this point, these ENV vars have not been loaded,
# causing the error. Below we define two throaway ENV vars to prevent the error from being thrown. These are then later overwritten,
# when all of the ENV vars are loaded.
ARG RAILS_MASTER_KEY

Check warning on line 35 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "RAILS_MASTER_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 35 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "RAILS_MASTER_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

ENV DOCUMENTS_S3_BUCKET=throwaway_value
ENV SCHOOLS_IMAGES_LOGOS_S3_BUCKET=throwaway_value
ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY

Check warning on line 39 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "RAILS_MASTER_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 39 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build docker image

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "RAILS_MASTER_KEY") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

RUN RAILS_ENV=production SECRET_KEY_BASE=required-to-run-but-not-used RAILS_SERVE_STATIC_FILES=1 bundle exec rake assets:precompile

Expand Down Expand Up @@ -67,4 +69,4 @@
ENV COMMIT_SHA=$COMMIT_SHA

EXPOSE 3000
CMD bundle exec rails db:migrate:ignore_concurrent_migration_exceptions && bundle exec rails s

Check warning on line 72 in Dockerfile

View workflow job for this annotation

GitHub Actions / Build docker image

JSON arguments recommended for ENTRYPOINT/CMD to prevent unintended behavior related to OS signals

JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals More info: https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ gem "high_voltage"
gem "httparty"
gem "ipaddr"
gem "jbuilder"
gem "jwt"
gem "kramdown"
gem "lockbox"
gem "mail-notify"
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ DEPENDENCIES
ipaddr
jbuilder
jsbundling-rails
jwt
kramdown
launchy (~> 3.0)
listen
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/jobseekers/account_transfers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ def create
flash[:success] = "Your account details have been transferred successfully!"
redirect_to jobseekers_profile_path
else
@email = @account_transfer_form.email
flash[:error] = "Account transfer failed. Please try again."
render :new
end
else
@email = @account_transfer_form.email
render :new
end
end
Expand All @@ -31,6 +33,8 @@ def successfully_transfer_account_data?
true
rescue Jobseekers::AccountTransfer::AccountNotFoundError, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e
Rails.logger.error("Account transfer failed: #{e.message}")
Rails.logger.error("Account transfer failed on #{e.record.class.name} with ID: #{e.record.id}")
Rails.logger.error("Validation errors: #{e.record.errors.full_messages.join(', ')}")
false
end
end
4 changes: 4 additions & 0 deletions app/controllers/jobseekers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ class Jobseekers::AccountsController < Jobseekers::BaseController
def show; end

def confirmation; end

def account_found; end

def account_not_found; end
end
77 changes: 77 additions & 0 deletions app/controllers/jobseekers/govuk_one_login_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
class Jobseekers::GovukOneLoginCallbacksController < Devise::OmniauthCallbacksController
include Jobseekers::GovukOneLogin::Errors
# Devise redirects response from Govuk One Login to this method.
# The request parameters contain the response from Govuk One Login from the user authentication through their portal.

# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def openid_connect
if (govuk_one_login_user = Jobseekers::GovukOneLogin::UserFromAuthResponse.call(params, session))
session[:govuk_one_login_id_token] = govuk_one_login_user.id_token
jobseeker = Jobseeker.find_by(govuk_one_login_id: govuk_one_login_user.id) ||
Jobseeker.find_by(email: govuk_one_login_user.email) # Pre-migration to GovUK One Login Jobseeker still non-linked with a One Login account.

# Completely new user
if jobseeker.nil?
session[:newly_created_user] = { value: "true", path: "/", expires: 1.hour.from_now }
jobseeker = Jobseeker.create_from_govuk_one_login(email: govuk_one_login_user.email, govuk_one_login_id: govuk_one_login_user.id)
# User exists but is their first time signing-in with OneLogin
elsif jobseeker.govuk_one_login_id.nil?
session[:user_exists_first_log_in] = { value: "true", path: "/", expires: 1.hour.from_now }
jobseeker.update(govuk_one_login_id: govuk_one_login_user.id)
# User changed their email in OneLogin after having already signed in with us
elsif jobseeker.email.downcase != govuk_one_login_user.email.downcase
jobseeker.update(email: govuk_one_login_user.email)
end

session.delete(:govuk_one_login_state)
session.delete(:govuk_one_login_nonce)

if jobseeker
sign_out_except(:jobseeker)
sign_in_and_redirect jobseeker
end
else
error_redirect
end
rescue GovukOneLoginError => e
Rails.logger.error(e.message)
error_redirect
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize

private

def error_redirect
return if jobseeker_signed_in?

flash[:alert] = "There was a problem signing in. Please try again."
redirect_to root_path
end

# Devise method to redirect the user after sign in.
# We need to build our own logic to redirect the user to the correct pages.
def after_sign_in_path_for(resource)
stored_location = stored_location_for(resource)
if redirect_to_location?(stored_location)
stored_location
elsif session[:newly_created_user]
session.delete(:newly_created_user)
account_not_found_jobseekers_account_path
elsif session[:user_exists_first_log_in]
session.delete(:user_exists_first_log_in)
account_found_jobseekers_account_path
else
jobseekers_job_applications_path
end
end

def redirect_to_location?(stored_location)
return false if stored_location.blank?

stored_location.include?("/job_application/new") || # Signed-in from a quick apply link
stored_location.include?("/saved_job/") || # Signed-in from a vacancy page save/unsave action.
stored_location.include?("/jobseekers/subscriptions") # Signed-in from a job alert email link.
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ def create
jobseeker.generate_merge_verification_code
Jobseekers::AccountMailer.request_account_transfer(jobseeker).deliver_now
end
redirect_to new_jobseekers_account_transfer_path(email: @request_account_transfer_email_form.email)

success_message = @request_account_transfer_email_form.email_resent ? t(".success") : nil
redirect_to new_jobseekers_account_transfer_path(email: @request_account_transfer_email_form.email), success: success_message
else
render :new
end
end

def request_account_transfer_email_form_params
params.require(:jobseekers_request_account_transfer_email_form).permit(:email)
params.require(:jobseekers_request_account_transfer_email_form).permit(:email, :email_resent)
end
end
4 changes: 1 addition & 3 deletions app/controllers/jobseekers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ class Jobseekers::SessionsController < Devise::SessionsController

def new
if (attempted_path = params[:attempted_path])
alert_text = t("jobseekers.forced_login.#{forced_login_resource(attempted_path)}_html",
account_creation_link: helpers.govuk_link_to(t("jobseekers.forced_login.create_account"), new_jobseeker_registration_url))
flash.now[:alert] = alert_text
flash.now[:alert] = t("jobseekers.forced_login.#{forced_login_resource(attempted_path)}_html")
elsif (login_failure = params[:login_failure])
alert_text = t("devise.failure.#{login_failure}")
trigger_jobseeker_sign_in_event(:failure, alert_text)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Jobseekers::RequestAccountTransferEmailForm < BaseForm
attr_accessor :email
attr_accessor :email, :email_resent

validates :email, presence: true
validates :email, email_address: true
Expand Down
20 changes: 20 additions & 0 deletions app/helpers/jobseekers_login_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module JobseekersLoginHelper
include Jobseekers::GovukOneLogin::Helper

def jobseeker_login_uri
params = generate_login_params
session[:govuk_one_login_state] = params[:state]
session[:govuk_one_login_nonce] = params[:nonce]

govuk_one_login_uri(:login, params)
end

def jobseeker_logout_uri
params = generate_logout_params(session[:govuk_one_login_id_token])
govuk_one_login_uri(:logout, params)
end

def jobseeker_login_button(class: "")
govuk_button_link_to(t("buttons.one_login_sign_in"), jobseeker_login_uri.to_s, class:)
end
end
13 changes: 13 additions & 0 deletions app/models/jobseeker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Jobseeker < ApplicationRecord
has_one :jobseeker_profile

validates :email, presence: true, email_address: true
validates :govuk_one_login_id, uniqueness: true, allow_nil: true

after_update :update_subscription_emails

Expand All @@ -35,6 +36,18 @@ def needs_email_confirmation?
!confirmed? || unconfirmed_email.present?
end

def self.create_from_govuk_one_login(email:, govuk_one_login_id:)
return unless email.present? && govuk_one_login_id.present?

# OneLogin users won't need/use this password. But is required by validations for in-house Devise users.
# Eventually when all the users become OneLogin users, we should be able to remove the password requirement.
random_password = Devise.friendly_token
create!(email: email.downcase,
govuk_one_login_id: govuk_one_login_id,
password: random_password,
confirmed_at: Time.zone.now)
end

def generate_merge_verification_code
self.account_merge_confirmation_code = SecureRandom.alphanumeric(6)
self.account_merge_confirmation_code_generated_at = Time.current
Expand Down
19 changes: 19 additions & 0 deletions app/services/jobseekers/govuk_one_login.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Jobseekers::GovukOneLogin
BASE_URL = Rails.application.config.govuk_one_login_base_url
CALLBACKS_BASE_URL = "#{ENV.fetch('DOMAIN').include?('localhost') ? 'http' : 'https'}://#{ENV.fetch('DOMAIN')}".freeze

CALLBACKS = {
login: "#{CALLBACKS_BASE_URL}/jobseekers/auth/govuk_one_login/callback",
logout: "#{CALLBACKS_BASE_URL}/jobseekers/sign_out",
}.freeze

ENDPOINTS = {
login: "#{BASE_URL}/authorize",
logout: "#{BASE_URL}/logout",
token: "#{BASE_URL}/token",
user_info: "#{BASE_URL}/userinfo",
jwks: "#{BASE_URL}/.well-known/jwks.json",
}.freeze

User = Struct.new(:id, :email, :id_token, keyword_init: true)
end
85 changes: 85 additions & 0 deletions app/services/jobseekers/govuk_one_login/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#
# - 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
#
# see https://docs.sign-in.service.gov.uk/
class Jobseekers::GovukOneLogin::Client
include Jobseekers::GovukOneLogin
include Jobseekers::GovukOneLogin::Errors

JWT_SIGNING_ALGORITHM = "RS256".freeze

attr_reader :code

def initialize(code)
@code = code
end

# POST /token
def tokens
uri, http = build_http(ENDPOINTS[:token])
request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/x-www-form-urlencoded" })
request.set_form_data({ grant_type: "authorization_code",
code: code,
redirect_uri: CALLBACKS[:login],
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: jwt_assertion })
response = http.request(request)
JSON.parse(response.body)
rescue StandardError => e
raise ClientRequestError.new("GovukOneLogin.tokens", e.message)
end

# GET /userinfo
def user_info(access_token)
uri, http = build_http(ENDPOINTS[:user_info])
request = Net::HTTP::Get.new(uri.path, { "Authorization" => "Bearer #{access_token}" })
response = http.request(request)
JSON.parse(response.body)
rescue StandardError => e
raise ClientRequestError.new("GovukOneLogin.user_info", e.message)
end

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, { verify_iat: true, algorithm: JWT_SIGNING_ALGORITHM })
end

private

def build_http(address)
uri = URI.parse(address)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
[uri, http]
end

# GET /.well-known/jwks.json
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))
JSON.parse(response.body)
end
end

def jwt_assertion
rsa_private = OpenSSL::PKey::RSA.new(Rails.application.config.govuk_one_login_private_key)
JWT.encode(jwt_payload, rsa_private, JWT_SIGNING_ALGORITHM)
end

def jwt_payload
{
aud: ENDPOINTS[:token],
iss: Rails.application.config.govuk_one_login_client_id,
sub: Rails.application.config.govuk_one_login_client_id,
exp: Time.zone.now.to_i + (5 * 60),
jti: SecureRandom.uuid,
iat: Time.zone.now.to_i,
}
end
end
14 changes: 14 additions & 0 deletions app/services/jobseekers/govuk_one_login/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Jobseekers::GovukOneLogin::Errors
class GovukOneLoginError < StandardError
def initialize(error = "GovukOneLogin", description = "Failed to authenticate with Govuk One Login")
super("#{error}: #{description}")
end
end

class ClientRequestError < GovukOneLoginError; end
class AuthenticationError < GovukOneLoginError; end
class SessionKeyError < GovukOneLoginError; end
class TokensError < GovukOneLoginError; end
class IdTokenError < GovukOneLoginError; end
class UserInfoError < GovukOneLoginError; end
end
Loading
Loading