-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add SwWebhookAuthentication middleware
- Loading branch information
1 parent
c5dfb1b
commit c820dd0
Showing
4 changed files
with
252 additions
and
0 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,53 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rack/media_type' | ||
|
||
module Rack | ||
|
||
class SwWebhookAuthentication | ||
|
||
FORM_URLENCODED_MEDIA_TYPE = Rack::MediaType.type('application/x-www-form-urlencoded') | ||
|
||
def initialize(app, private_key, *paths, &private_key_lookup) | ||
@app = app | ||
@private_key = private_key | ||
define_singleton_method(:get_private_key, private_key_lookup) if block_given? | ||
@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) | ||
original_url = request.url | ||
params = extract_params!(request) | ||
private_key = @private_key || get_private_key(params['AccountSid']) | ||
validator = Signalwire::Webhook::ValidateRequest.new(private_key) | ||
signature = env['HTTP_X_SIGNALWIRE_SIGNATURE'] || env['HTTP_X_TWILIO_SIGNATURE'] || '' | ||
if validator.validate(original_url, params, signature) | ||
@app.call(env) | ||
else | ||
[ | ||
403, | ||
{ 'Content-Type' => 'text/plain' }, | ||
['Signalwire Request Validation Failed.'] | ||
] | ||
end | ||
end | ||
|
||
def extract_params!(request) | ||
return {} unless request.post? | ||
|
||
if request.media_type == FORM_URLENCODED_MEDIA_TYPE | ||
request.POST | ||
else | ||
request.body.rewind | ||
body = request.body.read | ||
request.body.rewind | ||
body | ||
end | ||
end | ||
|
||
private :extract_params! | ||
|
||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
require 'spec_helper' | ||
require 'rack/mock' | ||
require 'rack/signalwire_webhook_authentication' | ||
|
||
describe Rack::SwWebhookAuthentication do | ||
before do | ||
@app = ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['Hello']] } | ||
end | ||
|
||
describe 'new' do | ||
it 'should initialize with an app, auth token and a path' do | ||
expect do | ||
Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/) | ||
end.not_to raise_error | ||
end | ||
|
||
it 'should initialize with an app, auth token and paths' do | ||
expect do | ||
Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/) | ||
end.not_to raise_error | ||
end | ||
|
||
it 'should initialize with an app, dynamic token and paths' do | ||
expect do | ||
Rack::SwWebhookAuthentication.new(@app, nil, /\/voice/, /\/sms/) | ||
end.not_to raise_error | ||
end | ||
end | ||
|
||
describe 'calling against one path with dynamic auth token' do | ||
it 'should allow a request through if it validates' do | ||
auth_token = 'qwerty' | ||
account_sid = 12_345 | ||
expect_any_instance_of(Rack::Request).to receive(:post?).and_return(true) | ||
expect_any_instance_of(Rack::Request).to receive(:media_type).and_return(Rack::MediaType.type('application/x-www-form-urlencoded')) | ||
expect_any_instance_of(Rack::Request).to receive(:POST).and_return({ 'AccountSid' => account_sid }) | ||
@middleware = Rack::SwWebhookAuthentication.new(@app, nil, /\/voice/) { |asid| auth_token } | ||
request_validator = double('RequestValidator') | ||
expect(Twilio::Security::RequestValidator).to receive(:new).with(auth_token).and_return(request_validator) | ||
expect(request_validator).to receive(:validate).and_return(true) | ||
request = Rack::MockRequest.env_for('/voice') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(200) | ||
end | ||
end | ||
|
||
describe 'calling against one path' do | ||
before do | ||
@middleware = Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/) | ||
end | ||
|
||
it 'should not intercept when the path doesn\'t match' do | ||
expect_any_instance_of(Twilio::Security::RequestValidator).to_not receive(:validate) | ||
request = Rack::MockRequest.env_for('/sms') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(200) | ||
end | ||
|
||
it 'should allow a request through if it validates' do | ||
expect_any_instance_of(Twilio::Security::RequestValidator).to( | ||
receive(:validate).and_return(true) | ||
) | ||
request = Rack::MockRequest.env_for('/voice') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(200) | ||
end | ||
|
||
it 'should short circuit a request to 403 if it does not validate' do | ||
expect_any_instance_of(Twilio::Security::RequestValidator).to( | ||
receive(:validate).and_return(false) | ||
) | ||
request = Rack::MockRequest.env_for('/voice') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(403) | ||
end | ||
end | ||
|
||
describe 'calling against many paths' do | ||
before do | ||
@middleware = Rack::SwWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/) | ||
end | ||
|
||
it 'should not intercept when the path doesn\'t match' do | ||
expect_any_instance_of(Twilio::Security::RequestValidator).to_not receive(:validate) | ||
request = Rack::MockRequest.env_for('icesms') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(200) | ||
end | ||
|
||
it 'shold allow a request through if it validates' do | ||
expect_any_instance_of(Twilio::Security::RequestValidator).to( | ||
receive(:validate).and_return(true) | ||
) | ||
request = Rack::MockRequest.env_for('/sms') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(200) | ||
end | ||
|
||
it 'should short circuit a request to 403 if it does not validate' do | ||
expect_any_instance_of(Twilio::Security::RequestValidator).to( | ||
receive(:validate).and_return(false) | ||
) | ||
request = Rack::MockRequest.env_for('/sms') | ||
status, headers, body = @middleware.call(request) | ||
expect(status).to be(403) | ||
end | ||
end | ||
|
||
describe 'validating non-form-data POST payloads' do | ||
it 'should fail if the body does not validate' do | ||
middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/) | ||
input = StringIO.new('{"message": "a post body that does not match the bodySHA256"}') | ||
|
||
request = Rack::MockRequest.env_for( | ||
'https://example.com/test?bodySHA256=79bfb0acaf0045fd30f13d48d4fe296b393d85a3bfbee881a0172b2bd574b11e', | ||
method: 'POST', | ||
input: input | ||
) | ||
request['HTTP_X_TWILIO_SIGNATURE'] = '+LYlbGr/VmN84YPJQCuWs+9UA7E=' | ||
request['CONTENT_TYPE'] = 'application/json' | ||
|
||
status, headers, body = middleware.call(request) | ||
|
||
expect(status).not_to be(200) | ||
end | ||
|
||
it 'should validate if the body signature is correct' do | ||
middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/) | ||
input = StringIO.new('{"message": "a post body"}') | ||
|
||
request = Rack::MockRequest.env_for( | ||
'https://example.com/test?bodySHA256=8d90d640c6ba47d595ac56203d7f5c6b511be80fdf44a2055acca75a119b9fd2', | ||
method: 'POST', | ||
input: input | ||
) | ||
request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'zR5Oq4f6cijN5oz5bisiVuxYnTU=' | ||
request['CONTENT_TYPE'] = 'application/json' | ||
|
||
status, headers, body = middleware.call(request) | ||
|
||
expect(status).to be(200) | ||
end | ||
|
||
it 'should validate even if a previous middleware read the body first' do | ||
middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/) | ||
input = StringIO.new('{"message": "a post body"}') | ||
|
||
request = Rack::MockRequest.env_for( | ||
'https://example.com/test?bodySHA256=8d90d640c6ba47d595ac56203d7f5c6b511be80fdf44a2055acca75a119b9fd2', | ||
method: 'POST', | ||
input: input | ||
) | ||
request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'zR5Oq4f6cijN5oz5bisiVuxYnTU=' | ||
request['CONTENT_TYPE'] = 'application/json' | ||
request['rack.input'].read | ||
|
||
status, headers, body = middleware.call(request) | ||
|
||
expect(status).to be(200) | ||
end | ||
end | ||
|
||
describe 'validating application/x-www-form-urlencoded POST payloads' do | ||
it 'should fail if the body does not validate' do | ||
middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/) | ||
|
||
request = Rack::MockRequest.env_for( | ||
'https://example.com/test', | ||
method: 'POST', | ||
params: { 'foo' => 'bar' } | ||
) | ||
request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'foobarbaz' | ||
expect(request['CONTENT_TYPE']).to eq('application/x-www-form-urlencoded') | ||
|
||
status, headers, body = middleware.call(request) | ||
|
||
expect(status).not_to be(200) | ||
end | ||
|
||
it 'should validate if the body signature is correct' do | ||
middleware = Rack::SwWebhookAuthentication.new(@app, 'qwerty', /\/test/) | ||
|
||
request = Rack::MockRequest.env_for( | ||
'https://example.com/test', | ||
method: 'POST', | ||
params: { 'foo' => 'bar' } | ||
) | ||
request['HTTP_X_SIGNALWIRE_SIGNATURE'] = 'TR9Skm9jiF4WVRJznU5glK5I83k=' | ||
expect(request['CONTENT_TYPE']).to eq('application/x-www-form-urlencoded') | ||
|
||
status, headers, body = middleware.call(request) | ||
|
||
expect(status).to be(200) | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
|
||
%w[ | ||
bundler/setup | ||
rack | ||
signalwire | ||
webmock/rspec | ||
vcr | ||
|