Skip to content

Instantly share code, notes, and snippets.

@armiiller
Last active March 9, 2021 14:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save armiiller/a82eb3bbd439b94d5fad5ab9140c449c to your computer and use it in GitHub Desktop.
Save armiiller/a82eb3bbd439b94d5fad5ab9140c449c to your computer and use it in GitHub Desktop.
# 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