Skip to content

Commit

Permalink
Restore accidentally removed files. (twilio#352)
Browse files Browse the repository at this point in the history
* Undeprecate Twilio.configure.
* Restore Webhook Auth helper.

This reverts commit 038a3f9.
  • Loading branch information
codejudas authored and dougblack committed Oct 13, 2017
1 parent 4772609 commit c5d98cd
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 4 deletions.
46 changes: 46 additions & 0 deletions lib/rack/twilio_webhook_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module Rack
# Middleware that authenticates webhooks from Twilio using the request
# validator.
#
# The middleware takes an auth token 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 checked for authentication.
#
# Example:
#
# require 'rack'
# use Rack::TwilioWebhookAuthentication, ENV['AUTH_TOKEN'], /\/messages/
#
# The above appends this middleware to the stack, using an auth token saved in
# the ENV and only against paths that match /\/messages/. 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 TwilioWebhookAuthentication
def initialize(app, auth_token, *paths, &auth_token_lookup)
@app = app
@auth_token = auth_token
define_singleton_method(:get_auth_token, auth_token_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 = request.post? ? request.POST : {}
auth_token = @auth_token || get_auth_token(params['AccountSid'])
validator = Twilio::Security::RequestValidator.new(auth_token)
signature = env['HTTP_X_TWILIO_SIGNATURE'] || ''
if validator.validate(original_url, params, signature)
@app.call(env)
else
[
403,
{ 'Content-Type' => 'text/plain' },
['Twilio Request Validation Failed.']
]
end
end
end
end
1 change: 1 addition & 0 deletions lib/twilio-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'json'

require 'twilio-ruby/version' unless defined?(Twilio::VERSION)
require 'rack/twilio_webhook_authentication'

require 'twilio-ruby/util'
require 'twilio-ruby/jwt/jwt'
Expand Down
2 changes: 1 addition & 1 deletion lib/twilio-ruby/security/request_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Twilio
module Security
class RequestValidator
def initialize(auth_token = nil)
@auth_token = auth_token
@auth_token = auth_token || Twilio.auth_token
raise ArgumentError, 'Auth token is required' if @auth_token.nil?
end

Expand Down
3 changes: 0 additions & 3 deletions lib/twilio-ruby/util/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ module Twilio
module Util
class Configuration
attr_accessor :account_sid, :auth_token

def account_sid=value
warn "'Twilio::Util::Configuration.account_sid' has been deprecated. Please do not use the configure block."
@account_sid=value
end

def auth_token=value
warn "'Twilio::Util::Configuration.auth_token' has been deprecated. Please do not use the configure block."
@auth_token=value
end
end
Expand Down
106 changes: 106 additions & 0 deletions spec/rack/twilio_webhook_authentication_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
require 'spec_helper'
require 'rack/mock'

describe Rack::TwilioWebhookAuthentication 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::TwilioWebhookAuthentication.new(@app, 'ABC', /\/voice/)
end.not_to raise_error
end

it 'should initialize with an app, auth token and paths' do
expect do
Rack::TwilioWebhookAuthentication.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::TwilioWebhookAuthentication.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(:POST).and_return({ 'AccountSid' => account_sid })
@middleware = Rack::TwilioWebhookAuthentication.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::TwilioWebhookAuthentication.new(@app, 'ABC', /\/voice/)
end

it 'should not intercept when the path doesn\'t match' do
expect(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::TwilioWebhookAuthentication.new(@app, 'ABC', /\/voice/, /\/sms/)
end

it 'should not intercept when the path doesn\'t match' do
expect(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
end
28 changes: 28 additions & 0 deletions spec/security/request_validator_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
require 'spec_helper'

describe Twilio::Security::RequestValidator do
describe 'configuration' do
after(:each) do
Twilio.instance_variable_set('@configuration', nil)
end

it 'should set the auth token from a configuration block' do
Twilio.configure do |config|
config.auth_token = 'someToken'
end

validator = Twilio::Security::RequestValidator.new
expect(validator.instance_variable_get('@auth_token')).to eq('someToken')
end

it 'should overwrite the auth token if passed to initializer' do
Twilio.configure do |config|
config.auth_token = 'someToken'
end

validator = Twilio::Security::RequestValidator.new 'otherToken'
expect(validator.instance_variable_get('@auth_token')).to eq('otherToken')
end

it 'should throw an argument error if the auth token isn\'t set' do
expect { Twilio::Security::RequestValidator.new }.to raise_error(ArgumentError)
end
end

describe 'validations' do
let(:token) { '2bd9e9638872de601313dc77410d3b23' }

Expand Down

0 comments on commit c5d98cd

Please sign in to comment.