From 46b85497628f0f56d34e6448c77a0a51208643b7 Mon Sep 17 00:00:00 2001 From: Stephen Daly Date: Thu, 25 Jun 2026 11:47:06 +0100 Subject: [PATCH 1/3] Generate JWK for One Login public key As part of the omniauth initializer, resolve the public key from the private key and convert it to JWK format. Store this on the application config so that we can retrieve it later when we need to serve it from the JWKS endpoint. Set the JWK kid on the omniauth configuration so it is sent with requests to One Login. --- config/initializers/omniauth.rb | 5 ++- spec/initializers/omniauth_spec.rb | 60 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 spec/initializers/omniauth_spec.rb diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index cb6da2995..ccb53317a 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -7,6 +7,9 @@ private_key_pem = private_key_pem.gsub('\n', "\n") private_key = OpenSSL::PKey::RSA.new(private_key_pem) + + public_key_jwk = JWT::JWK.new(private_key.public_key, use: "sig") + Rails.application.config.x.one_login.public_key_jwk = public_key_jwk end Rails.application.config.middleware.use OmniAuth::Builder do @@ -16,7 +19,7 @@ idp_base_url: Settings.govuk_one_login.base_url, private_key: private_key, redirect_uri: "/auth/govuk_one_login/callback", - private_key_kid: "", # TODO: we'll need to set this when we switch to using a JWKS endpoint + private_key_kid: public_key_jwk&.kid, signing_algorithm: "ES256", scope: "openid email", ui_locales: "en cy", diff --git a/spec/initializers/omniauth_spec.rb b/spec/initializers/omniauth_spec.rb new file mode 100644 index 000000000..df97de914 --- /dev/null +++ b/spec/initializers/omniauth_spec.rb @@ -0,0 +1,60 @@ +require "rails_helper" + +RSpec.describe "omniauth initializer" do + let(:initializer_path) { Rails.root.join("config/initializers/omniauth.rb") } + + let(:one_login_settings) do + double( + private_key: private_key_setting, + client_id: "test-client-id", + base_url: "https://oidc.integration.account.gov.uk", + ) + end + + before do + allow(Settings).to receive(:govuk_one_login).and_return(one_login_settings) + allow(Rails.application.config.middleware).to receive(:use) + allow(OmniAuth::GovukOneLogin::IdpConfiguration).to receive(:new).and_return(instance_double(OmniAuth::GovukOneLogin::IdpConfiguration)) + end + + around do |example| + original_public_key_jwk = Rails.application.config.x.one_login.public_key_jwk + original_idp_configuration = Rails.application.config.x.one_login.idp_configuration + example.run + ensure + Rails.application.config.x.one_login.public_key_jwk = original_public_key_jwk + Rails.application.config.x.one_login.idp_configuration = original_idp_configuration + end + + context "when the private key is present" do + let(:rsa_private_key) { OpenSSL::PKey::RSA.generate(2048) } + let(:private_key_setting) { Base64.encode64(rsa_private_key.to_pem) } + + before { load initializer_path } + + it "sets Rails.application.config.x.one_login.public_key_jwk" do + expect(Rails.application.config.x.one_login.public_key_jwk).to be_a(JWT::JWK::RSA) + end + + it "sets the JWK use to 'sig'" do + expect(Rails.application.config.x.one_login.public_key_jwk[:use]).to eq("sig") + end + + it "sets the JWK kid" do + expect(Rails.application.config.x.one_login.public_key_jwk.kid).to be_a(String) + end + end + + context "when the private key is absent" do + let(:private_key_setting) { nil } + + before do + Rails.application.config.x.one_login.public_key_jwk = nil + load initializer_path + end + + it "does not set Rails.application.config.x.one_login.public_key_jwk" do + expect(Rails.application.config.x.one_login.public_key_jwk).to be_nil + end + end +end From 05578fcca40727a86a8bcc7d78e18417080cce3d Mon Sep 17 00:00:00 2001 From: Stephen Daly Date: Thu, 25 Jun 2026 11:50:17 +0100 Subject: [PATCH 2/3] Add JKWS endpoint for GOV.UK One Login public key Add a publicly accessible endpoint that returns a JSON Web Key Set (JWKS) to share our public key with GOV.UK One Login. --- app/controllers/one_login_jwks_controller.rb | 6 ++++ config/routes.rb | 2 ++ .../one_login_jwks_controller_spec.rb | 34 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 app/controllers/one_login_jwks_controller.rb create mode 100644 spec/requests/one_login_jwks_controller_spec.rb diff --git a/app/controllers/one_login_jwks_controller.rb b/app/controllers/one_login_jwks_controller.rb new file mode 100644 index 000000000..4fe872eb4 --- /dev/null +++ b/app/controllers/one_login_jwks_controller.rb @@ -0,0 +1,6 @@ +class OneLoginJwksController < ApplicationController + def show + jwk = Rails.application.config.x.one_login.public_key_jwk + render json: JWT::JWK::Set.new(jwk).export + end +end diff --git a/config/routes.rb b/config/routes.rb index 5e3311b98..b4e7d0899 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,8 @@ get "/submission" => "submission_status#status", as: :status get "/.well-known/security.txt" => redirect("https://vulnerability-reporting.service.security.gov.uk/.well-known/security.txt") + get "/govuk-one-login-jwks", to: "one_login_jwks#show", as: :one_login_jwks + form_id_constraints = { form_id: UrlPatterns::FORM_ID_REGEX } form_constraints = { **form_id_constraints, diff --git a/spec/requests/one_login_jwks_controller_spec.rb b/spec/requests/one_login_jwks_controller_spec.rb new file mode 100644 index 000000000..97843bab9 --- /dev/null +++ b/spec/requests/one_login_jwks_controller_spec.rb @@ -0,0 +1,34 @@ +require "rails_helper" +require "base64" + +RSpec.describe OneLoginJwksController, type: :request do + describe "GET #show" do + before do + public_key = OpenSSL::PKey::RSA.generate(2048).public_key + public_key_jwk = JWT::JWK.new(public_key, use: "sig") + + one_login_config = ActiveSupport::OrderedOptions.new + one_login_config.public_key_jwk = public_key_jwk + + allow(Rails.application.config.x).to receive(:one_login).and_return(one_login_config) + end + + it "returns the JWKS JSON" do + get one_login_jwks_path + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq("application/json; charset=utf-8") + expect(response.parsed_body).to match({ + "keys" => [ + { + "e" => "AQAB", + "kty" => "RSA", + "use" => "sig", + "kid" => a_kind_of(String), + "n" => a_kind_of(String), + }, + ], + }) + end + end +end From 07a9d121f598cf27e896a93f1203ea513574e535 Mon Sep 17 00:00:00 2001 From: Stephen Daly Date: Thu, 25 Jun 2026 10:37:23 +0100 Subject: [PATCH 3/3] Use JWK thumbprint as the kid The kid is used to identify individual keys returned by the JWKS endpoint. As per the documentation for the gem (https://github.com/jwt/ruby-jwt) we can use the standardised JWK thumbprint as the kid for the JWK rather than the legacy custom approach the gem uses by default. --- config/application.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/application.rb b/config/application.rb index 359493b79..8f8f3476d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -89,5 +89,7 @@ class Application < Rails::Application I18n.available_locales = %i[en cy] I18n.default_locale = :en + + JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint end end