Last active
March 9, 2021 14:32
-
-
Save armiiller/a82eb3bbd439b94d5fad5ab9140c449c to your computer and use it in GitHub Desktop.
This file contains hidden or 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
# A webhook signing algorythim, generally based off slacks https://api.slack.com/authentication/verifying-requests-from-slack | |
# I think I used this as the template: https://github.com/slack-ruby/slack-ruby-client/blob/master/lib/slack/events/request.rb#L51 | |
# sign_and_send is what is being sent by the service providing the outgoing webhook service | |
# verify_and_process is what a recieving server would to to process the incoming webhook, you can also see a nodejs implementation here https://gist.github.com/armiiller/72e4729372036cd43536f4f799dd2b22 | |
BRAND = "acme-inc" # TODO, your brand | |
def sign_and_send | |
# sign the request with the customers auth token | |
# the customers auth token is a shared secret, you can use a has_secure_token on the model | |
signing_secret = self.auth_token | |
version_number = 'v0' # always v0 for now | |
timestamp = Time.now.to_i | |
headers = { | |
'Content-Type': 'application/json', | |
"x-#{BRAND}-timestamp": timestamp.to_s, | |
} | |
body = { | |
# TODO - your body content json / hash | |
}.to_h.sort.to_h # this step is important, sort the body hash in alphabetical order | |
body_json = body.to_json | |
sig_basestring = [version_number, timestamp, body_json].join(':') | |
digest = OpenSSL::Digest::SHA256.new | |
hex_hash = OpenSSL::HMAC.hexdigest(digest, signing_secret, sig_basestring) | |
computed_signature = [version_number, hex_hash].join('=') | |
headers["x-#{BRAND}-signature"] = computed_signature | |
response = HTTParty.post(self.url, body: body_json, headers: headers, timeout: 3) | |
end | |
def verify_and_process | |
signing_secret = self.auth_token | |
version_number = 'v0' # always v0 for now | |
timestamp = request.headers["x-#{brand}-timestamp"] | |
raw_body = request.body.read # raw body JSON string | |
if Time.at(timestamp.to_i) < 5.minutes.ago | |
# could be a replay attack | |
render nothing: true, status: :bad_request | |
return | |
end | |
sig_basestring = [version_number, timestamp, raw_body].join(':') | |
digest = OpenSSL::Digest::SHA256.new | |
hex_hash = OpenSSL::HMAC.hexdigest(digest, signing_secret, sig_basestring) | |
computed_signature = [version_number, hex_hash].join('=') | |
webhook_signature = request.headers["x-#{BRAND}-signature"] | |
if computed_signature != webhook_signature | |
render nothing: true, status: :unauthorized | |
end | |
# Webhook signature is legit, process it | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment