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

Webhook Signature Verification #90

Merged
merged 5 commits into from Aug 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -99,6 +99,16 @@ To prevent this, StripeEvent supports using HTTP Basic authentication on your we

This is only truly secure if your webhook endpoint is accessed over SSL, which Stripe strongly recommends anyway.

## Authenticating webhooks

Stripe will cryptographically sign webhook payloads with a signature that is included in a special header sent with the request. Verifying this signature lets your application properly authenticate the request originated from Stripe. To leverage this feature, please set the `signing_secret` configuration value:

```
StripeEvent.signing_secret = Rails.application.secrets.stripe_signing_secret
```

Please refer to Stripe's documentation for more details: https://stripe.com/docs/webhooks#signatures

## Configuration

If you have built an application that has multiple Stripe accounts--say, each of your customers has their own--you may want to define your own way of retrieving events from Stripe (e.g. perhaps you want to use the [account parameter](https://stripe.com/docs/connect/webhooks) from the top level to detect the customer for the event, then grab their specific API key). You can do this:
Expand Down
13 changes: 13 additions & 0 deletions app/controllers/stripe_event/webhook_controller.rb
Expand Up @@ -2,8 +2,10 @@ module StripeEvent
class WebhookController < ActionController::Base
if respond_to?(:before_action)
before_action :request_authentication
before_action :verify_signature
else
before_filter :request_authentication
before_filter :verify_signature
end

def event
Expand All @@ -28,5 +30,16 @@ def request_authentication
end
end
end

def verify_signature
if StripeEvent.signing_secret
payload = request.body.read
signature = request.headers['Stripe-Signature']

Stripe::Webhook::Signature.verify_header payload, signature, StripeEvent.signing_secret
end
rescue Stripe::SignatureVerificationError
head :bad_request
end
end
end
2 changes: 1 addition & 1 deletion lib/stripe_event.rb
Expand Up @@ -4,7 +4,7 @@

module StripeEvent
class << self
attr_accessor :adapter, :backend, :event_retriever, :namespace, :authentication_secret
attr_accessor :adapter, :backend, :event_retriever, :namespace, :authentication_secret, :signing_secret

def configure(&block)
raise ArgumentError, "must provide a block" unless block_given?
Expand Down
40 changes: 40 additions & 0 deletions spec/controllers/webhook_controller_spec.rb
Expand Up @@ -84,4 +84,44 @@ def webhook_with_secret(secret, params)
expect(response.code).to eq '200'
end
end

context "with a signing secret" do
def webhook_with_signature(signature, params)
request.env['HTTP_STRIPE_SIGNATURE'] = signature
webhook params
end

def generate_signature(secret)
payload = 'id=evt_charge_succeeded'
timestamp = Time.now.to_i
signature = Stripe::Webhook::Signature.send(:compute_signature, "#{timestamp}.#{payload}", secret)

"t=#{timestamp},v1=#{signature}"
end

let(:shared_secret) { 'secret' }

before(:each) { StripeEvent.signing_secret = shared_secret }
after(:each) { StripeEvent.signing_secret = nil }

it "rejects missing signature" do
webhook id: 'evt_charge_succeeded'

expect(response.code).to eq '400'
end

it "rejects invalid signature" do
webhook_with_signature "invalid signature", id: 'evt_charge_succeeded'

expect(response.code).to eq '400'
end

it "accepts valid signature" do
stub_event 'evt_charge_succeeded'

webhook_with_signature generate_signature(shared_secret), id: 'evt_charge_succeeded'

expect(response.code).to eq '200'
end
end
end