Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Some code for a ring middleware written in Clojure to validate Github webhook calls
;; (c) 2016 Stefan Kamphausen
;; Released under the Eclipse Public License as is common in the Clojure world.
;;; Summary
;; This gist shows how you can implement GitHub webhook validation as a Clojure ring middleware.
;;; Description
;; It is not a ready-made solution that you can just plug into your code. It makes some assumptions that
;; may or may not be valid in your situation. I don't want to turn this into a full-blown generic
;; open source library because of this. Instead I provide this gist so that you can take the parts
;; you need and put everything together the way you need.
;;; Notes
;; * Make sure that this code receives the body *before* e.g. the JSON parser parses the body of
;; the request.
;; * Note, that other middleware can have problems with the already slurped body.
;; * A maybe unsual aspect of this implementation is, that it allows several keys to be
;; configured and if one of them yields a valid signature, we consider it a valid request.
;; * Also, this middleware validates all POST request, no way of filtering e.g. based on URI.
;; You may want to adapt this code...
;; GitHub documentation
;; https://developer.github.com/webhooks/securing/
;; Valuable input for this code came from
;; http://stackoverflow.com/questions/3208160/how-to-generate-an-hmac-in-java-equivalent-to-a-python-example/11102291#11102291
;; http://stackoverflow.com/questions/31729163/clojure-or-java-equivalent-to-rubys-hmac-hexdigest
;; As far as I understand, StackOverflow content is published under the CC-BY-SA license:
;; https://creativecommons.org/licenses/by-sa/3.0/
;; So, I hereby credit A. Malabarba and markltbaker as influences. :-)
(ns de.skamphausen.gist.gh-validation
(:require
[clojure.tools.logging :as log])
(:import org.apache.commons.codec.binary.Hex
javax.crypto.Mac;
javax.crypto.spec.SecretKeySpec
java.nio.charset.StandardCharsets))
(def ^:const ^:private signing-algorithm "HmacSHA1")
;; Using memoize to optimize object creation
;; FIXME: would probably have to use fixed encoding for non-ascii secrets
(defn- get-signing-key* [secret]
(SecretKeySpec. (.getBytes secret (StandardCharsets/UTF_8))
signing-algorithm))
(def ^:private get-signing-key (memoize get-signing-key*))
(defn- get-mac* [signing-key]
(doto (Mac/getInstance signing-algorithm)
(.init signing-key)))
(def ^:private get-mac (memoize get-mac*))
(defn hmac [^String s signature secret]
(let [mac (get-mac (get-signing-key secret))]
;; MUST use .doFinal which resets mac so that it can be
;; reused!
(str "sha1="
(Hex/encodeHexString
(.doFinal mac (.getBytes s (StandardCharsets/UTF_8)))))))
(defn- validate-string [^String s signature secret]
(let [calculated (hmac s signature secret)]
(log/debug "Comparing received" signature "with calculated" calculated)
(= signature calculated)))
;; Warn: Body-stream can only be slurped once. Possible conflict with other ring middleware
(defn body-as-string [request]
(let [body (:body request)]
(if (string? body)
body
(slurp body))))
(defn- valid-github? [secrets body request]
(let [signature (get-in request [:headers "x-hub-signature"])]
(log/debug "Found signature" signature)
(cond
;; only care about post
(not (= :post (:request-method request)))
"no-validation-not-a-post"
;; No secrets defined, no need to validate
(not (seq secrets))
"no-validation-no-secrets"
;; we have no signature but secrets are defined -> fail
(and (not signature) (seq secrets))
false
;; must validate this content
:else
(some (partial validate-string body signature) secrets))))
(def default-invalid-response
{:status 400
:headers {"Content-Type" "text/plain"}
:body "Invalid X-Hub-Signature in request."})
(defn- create-secrets-vector
"Handle no secret, one secret or vector of secrets."
[secret]
;; did we have a secret at all? Might want to issue a warning here
;;(when (not secret)
;; (log/warn "No shared secret(s) defined. This is insecure."))
(if secret
(if (vector? secret)
secret
[secret])
[]))
;; For a more general interface, would allow to pass a dedicated
;; handler for invalid responses overriding the simple response
(defn wrap-github-validation
{:arglists '([handler] [handler options])}
[handler & [{:keys [secret invalid-response]
:or {secret nil
invalid-response default-invalid-response}}]]
(let [secs (create-secrets-vector secret)]
(fn [request]
(let [body (body-as-string request)
valid? (valid-github? secs body request)]
(if valid?
(do
(log/debug "Request validation OK: " valid?)
(handler (assoc request
:validation {:valid true
:validation valid?}
;; update body which must be an InputStream
;; FIXME Need (StandardCharsets/UTF_8)??
:body (io/input-stream (.getBytes body)))))
(do
(log/warn "Request invalid! (Maybe wrong shared secret?)")
(log/debug "Returning " invalid-response)
invalid-response))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment