Skip to content

Commit

Permalink
[RUBY-3375] Configure Govpay callback endpoint to authenticate both f…
Browse files Browse the repository at this point in the history
…ront and back office accounts (#1572)

* [RUBY-3375] Configure Govpay callback endpoint to authenticate both front and back office accounts

- Use ENV variable WCRS_GOVPAY_BACK_OFFICE_CALLBACK_WEBHOOK_SIGNING_SECRET for storing back office signing secret

- Updated `GovpayPaymentWebhookSignatureService` to generate and verify signatures for both front office and back office accounts.
  - Now returns a hash with both `front_office` and `back_office` signatures.
  - Updated error handling to remove `signature` from the Airbrake notification parameters as it did not exist and would not during error

- Modified `ValidateGovpayPaymentWebhookBodyService` to validate against both front office and back office signatures.

- Updated specs for `GovpayPaymentWebhookSignatureService`:

- Updated specs for `ValidateGovpayPaymentWebhookBodyService`:

* [RUBY-3387] Add  to CI environment variables in
  • Loading branch information
jjromeo authored Oct 1, 2024
1 parent 8412597 commit 2af4f8e
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 23 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit 2af4f8e

Please sign in to comment.