diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cd0a1121..d0a54ad9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: WCRS_USE_XVFB_FOR_WICKEDPDF: true ENV_VARIABLE_TEST_FEATURE: true WCRS_GOVPAY_CALLBACK_WEBHOOK_SIGNING_SECRET: foo + WCRS_GOVPAY_BACK_OFFICE_CALLBACK_WEBHOOK_SIGNING_SECRET: foo2 steps: # Downloads a copy of the code in your repository before running CI tests diff --git a/app/services/waste_carriers_engine/govpay_payment_webhook_signature_service.rb b/app/services/waste_carriers_engine/govpay_payment_webhook_signature_service.rb index 18305fcfe..8bc28e831 100644 --- a/app/services/waste_carriers_engine/govpay_payment_webhook_signature_service.rb +++ b/app/services/waste_carriers_engine/govpay_payment_webhook_signature_service.rb @@ -5,22 +5,33 @@ class GovpayPaymentWebhookSignatureService < BaseService class DigestFailure < StandardError; end def run(body:) - hmac_digest(body.to_s) + generate_signatures(body.to_s) rescue StandardError => e Rails.logger.error "Govpay payment webhook signature generation failed: #{e}" - Airbrake.notify(e, body:, signature:) + Airbrake.notify(e, body:) raise DigestFailure, e end private - def webhook_signing_secret - @webhook_signing_secret = ENV.fetch("WCRS_GOVPAY_CALLBACK_WEBHOOK_SIGNING_SECRET") + def generate_signatures(body) + { + front_office: hmac_digest(body, front_office_secret), + back_office: hmac_digest(body, back_office_secret) + } end - def hmac_digest(body) + def front_office_secret + ENV.fetch("WCRS_GOVPAY_CALLBACK_WEBHOOK_SIGNING_SECRET") + end + + def back_office_secret + ENV.fetch("WCRS_GOVPAY_BACK_OFFICE_CALLBACK_WEBHOOK_SIGNING_SECRET") + end + + def hmac_digest(body, secret) digest = OpenSSL::Digest.new("sha256") - OpenSSL::HMAC.hexdigest(digest, webhook_signing_secret, body) + OpenSSL::HMAC.hexdigest(digest, secret, body) end end end diff --git a/app/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service.rb b/app/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service.rb index a40ee4e03..e6ebffbf0 100644 --- a/app/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service.rb +++ b/app/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service.rb @@ -7,8 +7,9 @@ class ValidationFailure < StandardError; end def run(body:, signature:) raise ValidationFailure, "Missing expected signature" if signature.blank? - body_signature = GovpayPaymentWebhookSignatureService.run(body:) - return true if body_signature == signature + body_signatures = GovpayPaymentWebhookSignatureService.run(body:) + + return true if body_signatures[:front_office] == signature || body_signatures[:back_office] == signature raise ValidationFailure, "digest/signature header mismatch" end diff --git a/spec/services/waste_carriers_engine/govpay_payment_webhook_signature_service_spec.rb b/spec/services/waste_carriers_engine/govpay_payment_webhook_signature_service_spec.rb index e75b5dab4..2664f83e1 100644 --- a/spec/services/waste_carriers_engine/govpay_payment_webhook_signature_service_spec.rb +++ b/spec/services/waste_carriers_engine/govpay_payment_webhook_signature_service_spec.rb @@ -5,8 +5,8 @@ module WasteCarriersEngine RSpec.describe GovpayPaymentWebhookSignatureService do describe ".run" do - - let(:webhook_signing_secret) { ENV.fetch("WCRS_GOVPAY_CALLBACK_WEBHOOK_SIGNING_SECRET") } + let(:front_office_secret) { ENV.fetch("WCRS_GOVPAY_CALLBACK_WEBHOOK_SIGNING_SECRET") } + let(:back_office_secret) { ENV.fetch("WCRS_GOVPAY_BACK_OFFICE_CALLBACK_WEBHOOK_SIGNING_SECRET") } let(:digest) { OpenSSL::Digest.new("sha256") } subject(:run_service) { described_class.run(body: webhook_body) } @@ -27,13 +27,17 @@ module WasteCarriersEngine context "with a complete webhook body" do let(:webhook_body) { JSON.parse(file_fixture("govpay/webhook_payment_update_body.json").read).to_s } - let(:valid_signature) { OpenSSL::HMAC.hexdigest(digest, webhook_signing_secret, webhook_body) } + let(:valid_front_office_signature) { OpenSSL::HMAC.hexdigest(digest, front_office_secret, webhook_body) } + let(:valid_back_office_signature) { OpenSSL::HMAC.hexdigest(digest, back_office_secret, webhook_body) } - it { expect(run_service).to eq valid_signature } + it "returns correct signatures for both front office and back office" do + result = run_service + expect(result[:front_office]).to eq valid_front_office_signature + expect(result[:back_office]).to eq valid_back_office_signature + end it "does not report an error" do run_service - expect(Airbrake).not_to have_received(:notify) end end diff --git a/spec/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service_spec.rb b/spec/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service_spec.rb index 24271de62..09c3b76da 100644 --- a/spec/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service_spec.rb +++ b/spec/services/waste_carriers_engine/validate_govpay_payment_webhook_body_service_spec.rb @@ -5,20 +5,20 @@ module WasteCarriersEngine RSpec.describe ValidateGovpayPaymentWebhookBodyService do describe ".run" do - - let(:headers) { "Pay-Signature" => signature } let(:webhook_body) { JSON.parse(file_fixture("govpay/webhook_payment_update_body.json").read).to_s } - let(:webhook_signing_secret) { ENV.fetch("WCRS_GOVPAY_CALLBACK_WEBHOOK_SIGNING_SECRET") } - let(:digest) { OpenSSL::Digest.new("sha256") } - let(:valid_signature) { Faker::Number.hexadecimal(digits: 20) } + let(:valid_front_office_signature) { Faker::Number.hexadecimal(digits: 20) } + let(:valid_back_office_signature) { Faker::Number.hexadecimal(digits: 20) } let(:signature_service) { instance_double(GovpayPaymentWebhookSignatureService) } let(:signature) { nil } - subject(:run_service) { described_class.run(body: webhook_body, signature:) } + subject(:run_service) { described_class.run(body: webhook_body, signature: signature) } before do allow(GovpayPaymentWebhookSignatureService).to receive(:new).and_return(signature_service) - allow(signature_service).to receive(:run).and_return(valid_signature) + allow(signature_service).to receive(:run).and_return( + front_office: valid_front_office_signature, + back_office: valid_back_office_signature + ) allow(Airbrake).to receive(:notify) end @@ -29,7 +29,6 @@ module WasteCarriersEngine it "logs an error" do run_service - expect(Airbrake).to have_received(:notify) rescue ValidateGovpayPaymentWebhookBodyService::ValidationFailure # we expect the service to raise an exception as well as logging the error. @@ -48,14 +47,24 @@ module WasteCarriersEngine it_behaves_like "fails validation" end - context "with a valid signature" do - let(:signature) { valid_signature } + context "with a valid front office signature" do + let(:signature) { valid_front_office_signature } it { expect(run_service).to be true } it "does not report an error" do run_service + expect(Airbrake).not_to have_received(:notify) + end + end + context "with a valid back office signature" do + let(:signature) { valid_back_office_signature } + + it { expect(run_service).to be true } + + it "does not report an error" do + run_service expect(Airbrake).not_to have_received(:notify) end end