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

Basic authentication support #55

Merged
merged 9 commits into from Feb 25, 2015
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
23 changes: 23 additions & 0 deletions README.md
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
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, decent looking pull-request, but could we move this portion to more idiomatic ruby?

Either:

authenticate_or_request_with_http_basic do |username, password|
  password == StripeEvent.authentication_secret
end if StripeEvent.authentication_secret

OR

if StripeEvent.authentication_secret
  authenticate_or_request_with_http_basic do |username, password|
    password == StripeEvent.authentication_secret
  end
end

end
end

def event
StripeEvent.instrument(params)
head :ok
Expand Down
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
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
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