-
Notifications
You must be signed in to change notification settings - Fork 468
/
Copy pathtwilio_webhook_authentication.rb
90 lines (75 loc) · 2.7 KB
/
twilio_webhook_authentication.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# frozen_string_literal: true
require 'rack/media_type'
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
# Rack's FORM_DATA_MEDIA_TYPES can be modified to taste, so we're slightly
# more conservative in what we consider form data.
FORM_URLENCODED_MEDIA_TYPE = Rack::MediaType.type('application/x-www-form-urlencoded')
def initialize(app, auth_token, *paths, &auth_token_lookup)
@app = app
@auth_token = auth_token
define_singleton_method(:get_auth_token_for_sid, 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)
if valid_request?(env)
@app.call(env)
else
[
403,
{ 'Content-Type' => 'text/plain' },
['Twilio Request Validation Failed.']
]
end
end
def valid_request?(env)
request = Rack::Request.new(env)
original_url = request.url
params = extract_params!(request)
signature = env['HTTP_X_TWILIO_SIGNATURE'] || ''
validators = build_validators(params['AccountSid'])
validators.any? { |validator| validator.validate(original_url, params, signature) }
end
private
def build_validators(account_sid)
get_auth_tokens(account_sid).map do |auth_token|
Twilio::Security::RequestValidator.new(auth_token)
end
end
def get_auth_tokens(account_sid)
tokens = @auth_token || get_auth_token_for_sid(account_sid)
[tokens].flatten
end
# Extract the params from the the request that we can use to determine the
# signature. This _may_ modify the passed in request since it may read/rewind
# the body.
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
end
end