-
Notifications
You must be signed in to change notification settings - Fork 12
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
added webhook verification to mux_ruby + helper-func infrastructure #46
base: master
Are you sure you want to change the base?
Changes from 10 commits
b063406
21438bb
4037616
7b700e2
1904c9a
bfadbe5
2aacb99
296320a
06bdcdc
56a5684
9b111d4
70bdb43
9c91ef1
1d6ee7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
root = true | ||
|
||
[*.rb] | ||
indent_size = 2 | ||
indent_style = space | ||
insert_final_newline = true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require 'mux_ruby/helpers/webhook_verifier' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
require 'openssl' | ||
require 'securecompare' | ||
|
||
module MuxRuby | ||
module Helpers | ||
class WebhookVerifier | ||
DEFAULT_TOLERANCE = 300 | ||
HEADER_SCHEMES = [:v1].freeze | ||
|
||
# Initializer. | ||
# | ||
# @param [String] secret the webhook secret from your Mux control panel | ||
# @param [Integer] tolerance the signature timing tolerance, in seconds (generally leave as is) | ||
# @param [Array<Symbol>] header_schemes the list of accepted header schemes for this verifier | ||
def initialize(secret: nil, tolerance: DEFAULT_TOLERANCE, header_schemes: [:v1]) | ||
raise "secret '#{secret.inspect}' must be a String" unless secret.is_a?(String) | ||
raise "tolerance '#{tolerance.inspect}' must be a positive number." \ | ||
unless tolerance.is_a?(Integer) && tolerance > 0 | ||
raise "header schemes '#{header_schemes.inspect}' must all be in HEADER_SCHEMES: '#{HEADER_SCHEMES.inspect}'" \ | ||
unless header_schemes.all? { |h| HEADER_SCHEMES.include?(h) } | ||
|
||
@secret = secret.dup | ||
@tolerance = tolerance | ||
@header_schemes = header_schemes.map(&:to_s).sort.uniq.reverse | ||
end | ||
|
||
# Initializer. | ||
# | ||
# @param [String] request_body the raw, unmodified body of the request | ||
# @param [String] header the Mux-Signature header | ||
# @param [Time] current_timestamp (for test purposes) the current time expected for this webhook (defaults to `Time.utc`) | ||
# @return [Boolean] true if webhook is verified; false otherwise | ||
def verify(request_body:, header:, current_timestamp: Time.now.getutc) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember this from Ruby land circa 2013, but is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It specifies a named argument rather than a positional argument. I think they're clearer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mmcc this must have come after your Ruby time (it was right near the end of my Ruby time) -- keyword args, which are a fantastic language feature: https://thoughtbot.com/blog/ruby-2-keyword-arguments In this case calling In the past we would sometimes use an options hash and then have conditionals inside method to check for things that were passed into the options hash. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this just replaces the options hash. @dylanjha , I didn't know you were a Ruby guy. Oh, do I have code for you to review...! |
||
header_parts = self.kv_from_header(header) | ||
|
||
timestamp = header_parts['t'].to_i | ||
scheme_used = @header_schemes.detect { |scheme| !header_parts[scheme].nil? } | ||
mux_signature = header_parts[scheme_used] | ||
|
||
if timestamp.nil? || mux_signature.nil? | ||
false | ||
else | ||
case scheme_used | ||
when 'v1' | ||
expected_signature = self.compute_v1_signature("#{timestamp}.#{request_body}") | ||
if SecureCompare.compare(expected_signature, mux_signature) | ||
delta = current_timestamp.to_i - timestamp | ||
|
||
if (delta <= @tolerance) | ||
true | ||
else | ||
false | ||
end | ||
else | ||
false | ||
end | ||
else | ||
warn "Unhandled *but recognized* Mux signature format '#{scheme_used}'. Please contact Mux." | ||
false | ||
end | ||
end | ||
end | ||
|
||
private | ||
def kv_from_header(header) | ||
Hash[header.strip.gsub(/^Mux-Signature:/, "").strip.split(",").map { |tup| tup.split("=") }] | ||
end | ||
|
||
def compute_v1_signature(payload) | ||
OpenSSL::HMAC.hexdigest("SHA256", @secret, payload) | ||
end | ||
end | ||
end | ||
end | ||
eropple marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require 'mux_ruby/helpers/webhook_verifier' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
require 'openssl' | ||
require 'securecompare' | ||
|
||
module MuxRuby | ||
module Helpers | ||
class WebhookVerifier | ||
DEFAULT_TOLERANCE = 300 | ||
HEADER_SCHEMES = [:v1].freeze | ||
|
||
# Initializer. | ||
# | ||
# @param [String] secret the webhook secret from your Mux control panel | ||
# @param [Integer] tolerance the signature timing tolerance, in seconds (generally leave as is) | ||
# @param [Array<Symbol>] header_schemes the list of accepted header schemes for this verifier | ||
def initialize(secret: nil, tolerance: DEFAULT_TOLERANCE, header_schemes: [:v1]) | ||
raise "secret '#{secret.inspect}' must be a String" unless secret.is_a?(String) | ||
raise "tolerance '#{tolerance.inspect}' must be a positive number." \ | ||
unless tolerance.is_a?(Integer) && tolerance > 0 | ||
raise "header schemes '#{header_schemes.inspect}' must all be in HEADER_SCHEMES: '#{HEADER_SCHEMES.inspect}'" \ | ||
unless header_schemes.all? { |h| HEADER_SCHEMES.include?(h) } | ||
|
||
@secret = secret.dup | ||
@tolerance = tolerance | ||
@header_schemes = header_schemes.map(&:to_s).sort.uniq.reverse | ||
end | ||
|
||
# Initializer. | ||
# | ||
# @param [String] request_body the raw, unmodified body of the request | ||
# @param [String] header the Mux-Signature header | ||
# @param [Time] current_timestamp (for test purposes) the current time expected for this webhook (defaults to `Time.utc`) | ||
# @return [Boolean] true if webhook is verified; false otherwise | ||
def verify(request_body:, header:, current_timestamp: Time.now.getutc) | ||
header_parts = self.kv_from_header(header) | ||
|
||
timestamp = header_parts['t'].to_i | ||
scheme_used = @header_schemes.detect { |scheme| !header_parts[scheme].nil? } | ||
mux_signature = header_parts[scheme_used] | ||
|
||
if timestamp.nil? || mux_signature.nil? | ||
false | ||
else | ||
case scheme_used | ||
when 'v1' | ||
expected_signature = self.compute_v1_signature("#{timestamp}.#{request_body}") | ||
if SecureCompare.compare(expected_signature, mux_signature) | ||
delta = current_timestamp.to_i - timestamp | ||
|
||
if (delta <= @tolerance) | ||
true | ||
else | ||
false | ||
end | ||
else | ||
false | ||
end | ||
else | ||
warn "Unhandled *but recognized* Mux signature format '#{scheme_used}'. Please contact Mux." | ||
false | ||
end | ||
end | ||
end | ||
|
||
private | ||
def kv_from_header(header) | ||
Hash[header.strip.gsub(/^Mux-Signature:/, "").strip.split(",").map { |tup| tup.split("=") }] | ||
end | ||
|
||
def compute_v1_signature(payload) | ||
OpenSSL::HMAC.hexdigest("SHA256", @secret, payload) | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we use
tolerance
anywhere else, typically it'sexpiration
. Why the change here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's tolerance in the node SDK.