From 52bb405eb8d036fd0ef4b472941470c06b8cd442 Mon Sep 17 00:00:00 2001 From: Ray McDermott Date: Mon, 4 Sep 2017 11:05:40 +0200 Subject: [PATCH 1/2] Support JWK for obtaining public keys --- project.clj | 5 ++- src/buddy/sign/jwk.clj | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/buddy/sign/jwk.clj diff --git a/project.clj b/project.clj index 64f8b48..f6a44ec 100644 --- a/project.clj +++ b/project.clj @@ -7,7 +7,10 @@ [com.taoensso/nippy "2.13.0" :scope "provided"] [org.clojure/test.check "0.9.0" :scope "test"] [buddy/buddy-core "1.4.0"] - [cheshire "5.8.0"]] + [cheshire "5.8.0"] + [org.clojure/core.cache "0.6.5"] + [aleph "0.4.3"] + [byte-streams "0.2.3"]] :source-paths ["src"] :javac-options ["-target" "1.7" "-source" "1.7" "-Xlint:-options"] :test-paths ["test"]) diff --git a/src/buddy/sign/jwk.clj b/src/buddy/sign/jwk.clj new file mode 100644 index 0000000..af462f8 --- /dev/null +++ b/src/buddy/sign/jwk.clj @@ -0,0 +1,84 @@ +;; Copyright (c) 2014-2016 Andrey Antukh +;; +;; Licensed under the Apache License, Version 2.0 (the "License") +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. + +;; Links to rfcs: +;; https://tools.ietf.org/html/rfc7517 + +(ns buddy.sign.jwk + (:require [cheshire.core :as json] + [byte-streams :as streams] + [clojure.core.cache :as cache] + [aleph.http :as http] + [buddy.core.keys :as keys] + [clojure.string :as string])) + + +(defn- string->edn + "Parse JSON from a string returning an edn map, otherwise nil" + [string] + (when-let [edn (json/decode string true)] + (when (map? edn) + edn))) + +(defn- oidc-data + "Obtain JWK discovery data and parse it into a Clojure map" + [endpoint] + (-> @(http/get endpoint) + :body + streams/to-string + string->edn)) + +(def ^:private one-day + (* 1000 ;milliseconds -> seconds + 60 ;seconds -> minutes + 60 ;mins -> hours + 24)) + +;; It is not necessary or efficient to obtain these discovery docs on every verification +;; Compose the behaviour of these two caches to limit the number of and length of time +;; that certs that can be held in the cache +(def ^:private discovery-cache (atom (-> {} + (cache/fifo-cache-factory) + (cache/ttl-cache-factory :ttl one-day)))) + +(defn- jwks + "Obtain the JWKs based on the issuer's .well-known URL, cache the result" + [well-known-endpoint] + (if (cache/has? @discovery-cache well-known-endpoint) + (get (cache/hit @discovery-cache well-known-endpoint) well-known-endpoint) + (when-let [discovery-doc (oidc-data well-known-endpoint)] + (let [updated-cache (swap! discovery-cache + #(cache/miss % well-known-endpoint discovery-doc))] + (get updated-cache well-known-endpoint))))) + +(defn- cert->pem + [cert] + (keys/str->public-key + (str "-----BEGIN CERTIFICATE-----\n" + (string/join "\n" (string/join "\n" (re-seq #".{1,64}" cert))) + "\n-----END CERTIFICATE-----\n"))) + +(defn get-public-key + "Obtain the JWK public key from the well-known endpoint that matches the kid" + [well-known-endpoint kid] + (when-let [jwks-doc (jwks well-known-endpoint)] + (when-let [signing-key (first (filter #(= kid (:kid %)) (:keys jwks-doc)))] + (cert->pem (first (:x5c signing-key)))))) + +;Sample usage +; +;(when-let [jwt-header (jws/decode-header jwt)] +; (when-let [public-key (jwk/get-public-key jwks-endpoint (:kid jwt-header))] +; (when-let [jwt-claims (jwt/unsign jwt public-key {:alg (:alg jwt-header)})] +; .... From 8c6410cee889072df06c4fb476080aac48d39067 Mon Sep 17 00:00:00 2001 From: Ray McDermott Date: Mon, 4 Sep 2017 11:09:58 +0200 Subject: [PATCH 2/2] Tweak the fn names --- src/buddy/sign/jwk.clj | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/buddy/sign/jwk.clj b/src/buddy/sign/jwk.clj index af462f8..0a5ad32 100644 --- a/src/buddy/sign/jwk.clj +++ b/src/buddy/sign/jwk.clj @@ -31,8 +31,8 @@ (when (map? edn) edn))) -(defn- oidc-data - "Obtain JWK discovery data and parse it into a Clojure map" +(defn- fetch + "Obtain HTTP resource and parse it into a Clojure map" [endpoint] (-> @(http/get endpoint) :body @@ -48,18 +48,17 @@ ;; It is not necessary or efficient to obtain these discovery docs on every verification ;; Compose the behaviour of these two caches to limit the number of and length of time ;; that certs that can be held in the cache -(def ^:private discovery-cache (atom (-> {} - (cache/fifo-cache-factory) - (cache/ttl-cache-factory :ttl one-day)))) +(def ^:private jwk-cache (atom (-> {} + (cache/fifo-cache-factory) + (cache/ttl-cache-factory :ttl one-day)))) -(defn- jwks +(defn- fetch-jwk "Obtain the JWKs based on the issuer's .well-known URL, cache the result" [well-known-endpoint] - (if (cache/has? @discovery-cache well-known-endpoint) - (get (cache/hit @discovery-cache well-known-endpoint) well-known-endpoint) - (when-let [discovery-doc (oidc-data well-known-endpoint)] - (let [updated-cache (swap! discovery-cache - #(cache/miss % well-known-endpoint discovery-doc))] + (if (cache/has? @jwk-cache well-known-endpoint) + (get (cache/hit @jwk-cache well-known-endpoint) well-known-endpoint) + (when-let [discovery-doc (fetch well-known-endpoint)] + (let [updated-cache (swap! jwk-cache #(cache/miss % well-known-endpoint discovery-doc))] (get updated-cache well-known-endpoint))))) (defn- cert->pem @@ -72,7 +71,7 @@ (defn get-public-key "Obtain the JWK public key from the well-known endpoint that matches the kid" [well-known-endpoint kid] - (when-let [jwks-doc (jwks well-known-endpoint)] + (when-let [jwks-doc (fetch-jwk well-known-endpoint)] (when-let [signing-key (first (filter #(= kid (:kid %)) (:keys jwks-doc)))] (cert->pem (first (:x5c signing-key))))))