-
Notifications
You must be signed in to change notification settings - Fork 322
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds rack middleware to make request verification easier in rack apps.
This adds a class that can be used to verify requests with a public key in rack applications like Rails or Sinatra. This also changed the class to fail verification if an error was thrown. In the case where a signature was missing, OpenSSL would throw an error instead of returning false for a invalid signature.
- Loading branch information
Showing
7 changed files
with
233 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# frozen_string_literal: true | ||
|
||
module Rack | ||
# Middleware that verifies webhooks from SendGrid using the EventWebhook | ||
# verifier. | ||
# | ||
# The middleware takes a public key with which to set up the request | ||
# validator and any number of paths. When a path matches the incoming request | ||
# path, the request will be verified using the signature and timestamp of the | ||
# request. | ||
# | ||
# Example: | ||
# | ||
# require 'rack' | ||
# use Rack::SendGridWebhookVerification, ENV['PUBLIC_KEY'], /\/emails/ | ||
# | ||
# The above appends this middleware to the stack, using a public key saved in | ||
# the ENV and only against paths that match /\/emails/. If the request | ||
# validates then it gets passed on to the action as normal. If the request | ||
# doesn't validate then the middleware responds immediately with a 403 status. | ||
class SendGridWebhookVerification | ||
def initialize(app, public_key, *paths) | ||
@app = app | ||
@public_key = public_key | ||
@path_regex = Regexp.union(paths) | ||
end | ||
|
||
def call(env) | ||
return @app.call(env) unless env['PATH_INFO'].match(@path_regex) | ||
request = Rack::Request.new(env) | ||
|
||
event_webhook = SendGrid::EventWebhook.new | ||
ec_public_key = event_webhook.convert_public_key_to_ecdsa(@public_key) | ||
verified = event_webhook.verify_signature( | ||
ec_public_key, | ||
request.body.read, | ||
request.env[SendGrid::EventWebhookHeader::SIGNATURE], | ||
request.env[SendGrid::EventWebhookHeader::TIMESTAMP] | ||
) | ||
|
||
if verified | ||
return @app.call(env) | ||
else | ||
return [ | ||
403, | ||
{ 'Content-Type' => 'text/plain' }, | ||
['SendGrid Request Verification Failed.'] | ||
] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
require "json" | ||
|
||
module Fixtures | ||
module EventWebhook | ||
PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==' | ||
FAILING_PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqTxd43gyp8IOEto2LdIfjRQrIbsd4SXZkLW6jDutdhXSJCWHw8REntlo7aNDthvj+y7GjUuFDb/R1NGe1OPzpA==' | ||
SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=' | ||
FAILING_SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=' | ||
TIMESTAMP = '1588788367' | ||
PAYLOAD = { | ||
'category'=>'example_payload', | ||
'event'=>'test_event', | ||
'message_id'=>'message_id', | ||
}.to_json | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
require 'spec_helper' | ||
require 'rack/mock' | ||
require './spec/fixtures/event_webhook' | ||
|
||
unless RUBY_PLATFORM == 'java' | ||
describe Rack::SendGridWebhookVerification do | ||
let(:public_key) { Fixtures::EventWebhook::PUBLIC_KEY } | ||
before do | ||
@app = ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['Hello']] } | ||
end | ||
|
||
describe 'new' do | ||
it 'should initialize with an app, public key and a path' do | ||
expect do | ||
Rack::SendGridWebhookVerification.new(@app, 'ABC', /\/email/) | ||
end.not_to raise_error | ||
end | ||
|
||
it 'should initialize with an app, public keys and paths' do | ||
expect do | ||
Rack::SendGridWebhookVerification.new(@app, 'ABC', /\/email/, /\/event/) | ||
end.not_to raise_error | ||
end | ||
end | ||
|
||
describe 'calling against one path' do | ||
let(:middleware) { Rack::SendGridWebhookVerification.new(@app, public_key, /\/email/) } | ||
|
||
it "should not intercept when the path doesn't match" do | ||
expect(SendGrid::EventWebhook).to_not receive(:new) | ||
request = Rack::MockRequest.env_for('/login') | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(200) | ||
end | ||
|
||
it 'should allow a request through if it is verified' do | ||
options = { | ||
:input => Fixtures::EventWebhook::PAYLOAD, | ||
'Content-Type' => "application/json" | ||
} | ||
options[SendGrid::EventWebhookHeader::SIGNATURE] = Fixtures::EventWebhook::SIGNATURE | ||
options[SendGrid::EventWebhookHeader::TIMESTAMP] = Fixtures::EventWebhook::TIMESTAMP | ||
request = Rack::MockRequest.env_for('/email', options) | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(200) | ||
end | ||
|
||
it 'should short circuit a request to 403 if there is no signature or timestamp' do | ||
options = { | ||
:input => Fixtures::EventWebhook::PAYLOAD, | ||
'Content-Type' => "application/json" | ||
} | ||
request = Rack::MockRequest.env_for('/email', options) | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(403) | ||
end | ||
|
||
it 'should short circuit a request to 403 if the signature is incorrect' do | ||
options = { | ||
:input => Fixtures::EventWebhook::PAYLOAD, | ||
'Content-Type' => "application/json" | ||
} | ||
options[SendGrid::EventWebhookHeader::SIGNATURE] = Fixtures::EventWebhook::FAILING_SIGNATURE | ||
options[SendGrid::EventWebhookHeader::TIMESTAMP] = Fixtures::EventWebhook::TIMESTAMP | ||
request = Rack::MockRequest.env_for('/email', options) | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(403) | ||
end | ||
|
||
it 'should short circuit a request to 403 if the payload is incorrect' do | ||
options = { | ||
:input => 'payload', | ||
'Content-Type' => "application/json" | ||
} | ||
options[SendGrid::EventWebhookHeader::SIGNATURE] = Fixtures::EventWebhook::SIGNATURE | ||
options[SendGrid::EventWebhookHeader::TIMESTAMP] = Fixtures::EventWebhook::TIMESTAMP | ||
request = Rack::MockRequest.env_for('/email', options) | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(403) | ||
end | ||
end | ||
|
||
describe 'calling with multiple paths' do | ||
let(:middleware) { Rack::SendGridWebhookVerification.new(@app, public_key, /\/email/, /\/events/) } | ||
|
||
it "should not intercept when the path doesn't match" do | ||
expect(SendGrid::EventWebhook).to_not receive(:new) | ||
request = Rack::MockRequest.env_for('/sms_events') | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(200) | ||
end | ||
|
||
it 'should allow a request through if it is verified' do | ||
options = { | ||
:input => Fixtures::EventWebhook::PAYLOAD, | ||
'Content-Type' => "application/json" | ||
} | ||
options[SendGrid::EventWebhookHeader::SIGNATURE] = Fixtures::EventWebhook::SIGNATURE | ||
options[SendGrid::EventWebhookHeader::TIMESTAMP] = Fixtures::EventWebhook::TIMESTAMP | ||
request = Rack::MockRequest.env_for('/events', options) | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(200) | ||
end | ||
|
||
it 'should short circuit a request to 403 if there is no signature or timestamp' do | ||
options = { | ||
:input => Fixtures::EventWebhook::PAYLOAD, | ||
'Content-Type' => "application/json" | ||
} | ||
request = Rack::MockRequest.env_for('/events', options) | ||
status, headers, body = middleware.call(request) | ||
expect(status).to eq(403) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters