Skip to content

Commit

Permalink
Merge pull request #55 from brentdax/feature/basic-auth
Browse files Browse the repository at this point in the history
Basic authentication support
  • Loading branch information
rmm5t committed Feb 25, 2015
2 parents 742fd9f + bed6d16 commit 720878d
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 1 deletion.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@ StripeEvent.subscribe 'customer.card.' do |event|
end
```

## Securing your webhook endpoint

StripeEvent automatically fetches events from Stripe to ensure they haven't been forged. However, that doesn't prevent an attacker who knows your endpoint name and an event's ID from forcing your server to process a legitimate event twice. If that event triggers some useful action, like generating a license key or enabling a delinquent account, you could end up giving something the attacker is supposed to pay for away for free.

To prevent this, StripeEvent supports using HTTP Basic authentication on your webhook endpoint. If only Stripe knows the basic authentication password, this ensures that the request really comes from Stripe. Here's what you do:

1. Arrange for a secret key to be available in your application's environment variables or `secrets.yml` file. You can generate a suitable secret with the `rake secret` command. (Remember, the `secrets.yml` file shouldn't contain production secrets directly; it should use ERB to include them.)

2. Configure StripeEvent to require that secret be used as a basic authentication password, using code along the lines of these examples:

```ruby
# STRIPE_WEBHOOK_SECRET environment variable
StripeEvent.authentication_secret = ENV['STRIPE_WEBHOOK_SECRET']
# stripe_webhook_secret key in secrets.yml file
StripeEvent.authentication_secret = Rails.application.secrets.stripe_webhook_secret
```

3. When you specify your webhook's URL in Stripe's settings, include the secret as a password in the URL, along with any username:

https://stripe:my-secret-key@myapplication.com/my-webhook-path

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

## 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 [user_id parameter](https://stripe.com/docs/apps/getting-started#webhooks) from the top level to detect the customer for the event, then grab their specific API key). You can do this:
Expand Down
8 changes: 8 additions & 0 deletions app/controllers/stripe_event/webhook_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
module StripeEvent
class WebhookController < ActionController::Base
before_filter do
if StripeEvent.authentication_secret
authenticate_or_request_with_http_basic do |username, password|
password == StripeEvent.authentication_secret
end
end
end

def event
StripeEvent.instrument(params)
head :ok
Expand Down
2 changes: 1 addition & 1 deletion lib/stripe_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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

def configure(&block)
raise ArgumentError, "must provide a block" unless block_given?
Expand Down
31 changes: 31 additions & 0 deletions spec/controllers/webhook_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,35 @@ def webhook(params)

expect { webhook id: 'evt_charge_succeeded' }.to raise_error(Stripe::StripeError, /testing/)
end

context "with an authentication secret" do
def webhook_with_secret(secret, params)
request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('user', secret)
webhook params
end

before(:each) { StripeEvent.authentication_secret = "secret" }
after(:each) { StripeEvent.authentication_secret = nil }

it "rejects requests with no secret" do
stub_event('evt_charge_succeeded')

webhook id: 'evt_charge_succeeded'
expect(response.code).to eq '401'
end

it "rejects requests with incorrect secret" do
stub_event('evt_charge_succeeded')

webhook_with_secret 'incorrect', id: 'evt_charge_succeeded'
expect(response.code).to eq '401'
end

it "accepts requests with correct secret" do
stub_event('evt_charge_succeeded')

webhook_with_secret 'secret', id: 'evt_charge_succeeded'
expect(response.code).to eq '200'
end
end
end

0 comments on commit 720878d

Please sign in to comment.