Skip to content

Instantly share code, notes, and snippets.

@ska2342
Last active May 11, 2022 21:15
Show Gist options
  • Save ska2342/4567b02531ff611db6a1208ebd4316e6 to your computer and use it in GitHub Desktop.
Save ska2342/4567b02531ff611db6a1208ebd4316e6 to your computer and use it in GitHub Desktop.
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))))))
@klauswuestefeld
Copy link

Hi, this is a simpler alternative where you validate the secret in the webhook uri:

(ns jux.http.github-webhook
  (:require [clojure.string :refer [split]]))

(defn- valid-webhook-request? [secret request]
  (and
   (-> request :uri (.startsWith "/github-webhook/"))
   (-> request :uri (split #"/") last (= secret))))

(defn- trigger [action]
  (action)
  {:status 200})

(defn wrap [delegate action secret]
  (fn [request]
    (if (valid-webhook-request? secret request)
      (trigger action)
      (delegate request))))

Usage:

(jux.http.github-webhook/wrap do-something-important! "your-secret-goes-here")

Configure your Github webhook with the secret in the URI: https://yourserver.com/github-webhook/your-secret-goes-here
Use the Enable SSL Verification option. Only github admins can see the webhook config page anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment