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/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 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/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/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 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