clj_jwt.clj 4.74 KB
Newer Older
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14
(ns no.nsd.clj-jwt
  (:require [buddy.core.keys :as keys]
            [buddy.core.keys.jwk.proto :as buddy-jwk]
            [buddy.sign.jwt :as jwt]
            [clojure.algo.generic.functor :refer [fmap]]
            [clojure.data.json :as json]
            [clojure.java.io :refer [resource]]
            [clojure.spec.gen.alpha :as gen]
            [clojure.spec.alpha :as s]
            [invetica.uri :as uri]))


(def jwtregex  #"^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?$")

15
(s/def ::sub (s/nilable string?))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
16

17 18
(s/def ::kid (s/with-gen (s/nilable string?)
                         #(s/gen #{"test-key"})))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
19 20 21 22 23

(s/def ::scope (s/nilable string?))

(s/def ::scopes (s/nilable (s/coll-of string? :kind set?)))

24 25
(s/def ::exp (s/nilable (s/and integer?
                               pos?)))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
26

27 28 29
(s/def ::kty (s/with-gen (s/nilable (s/and string?
                                           #(= "RSA" %)))
               #(s/gen #{"RSA" nil})))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
30 31 32 33 34 35 36 37 38

(s/def ::n (s/with-gen string?
             #(s/gen #{;;valid:
                       "nZq9S6leC-8Se5-VlHcVZ0HVpQRwFNuZRp82WFddhMZUoEEKybuiym6uNh5kquNADbZcRw4yxJI3BBuWLoOz-YBjXlnxNqeQgr2E8LZ_AsT-6Yb6xdKrZ5acXaLAQsXwk53GHhUcFzOFu3u6BXVMknCY6jI6dxgOlSlWQV2nCjWTio_cTbDjsSSfIQ9jWcK9aCmw37omCZqIXlLwGA9fD4Ah8c4-QTfV7dZ7q_MQmrCqv88_eYAvg-lUlUQRnB9jGg53MWlitYGKW_aUr8oRn7nHm-gsXtL_bzWLxSSbkxiht52e4mcFNOXAqXVlocW1YJC3weRojI-CXJZ6218z6Q"
                       ;;invalid:
                       "xnmcbvjksdhfwiuerfsdjbsdkjfghwileugkhjbvnxdvjbvwiuerhslkdjbvvklwl4iuhjxcvxnmbvkwerjlfhiwuerhsjdkdfkjbvwe4riefslkv-dlsfkjhwpoiefhcvsdjkhvowpefwoeifhv_sdøflkhjwpeoifhsvøkl"})))

(s/def ::e string?)

39 40 41 42
(s/def ::claims (s/nilable (s/keys  :opt-un [::exp
                                             ::scope
                                             ::scopes
                                             ::sub])))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
43 44 45 46

(s/def ::jwt (s/nilable (s/and string?
                               #(re-matches jwtregex %))))

47 48
(s/def ::jwt-header (s/keys :req-un [::kid ::kty]))

Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
49 50 51 52 53 54 55 56 57 58 59
(s/def ::jwk (s/keys :req-un [::kty ::e ::n ::kid]))

(s/def ::RSAPublicKey keys/public-key?)

(s/def ::key-store (s/map-of ::kid
                             ::RSAPublicKey))

(s/def ::resource (s/with-gen #(instance? java.net.URL %)
                   #(s/gen #{(resource "jwks.json")
                             (resource "jwks-other.json")})))

60 61 62
(s/def ::jwks-url (s/or :url  :invetica.uri/absolute-uri
                              :resource ::resource))

Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
63 64 65 66 67

(s/fdef jwks-edn->public-keys
        :args (s/cat :jwks (s/coll-of ::jwk :type vector?))
        :ret ::key-store)

68
(defn- jwks-edn->public-keys
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
69 70 71 72 73 74 75 76 77 78 79
  "Transform vector of json-web-keys to map of kid -> PublicKey pairs."
  [json-web-keys]
  (->> json-web-keys
       :keys
       (filter #(= (:kty %) "RSA"))
       (group-by :kid)
       (fmap first)
       (fmap buddy-jwk/jwk->public-key)))


(s/fdef fetch-keys
80 81 82 83 84 85 86 87 88
        :args (s/cat :jwks-url ::jwks-url)
        :ret  (s/with-gen ::key-store
                          #(s/gen #{(->> (resource "jwks.json")
                                         slurp
                                         ((fn [jwks-string] (json/read-str jwks-string :key-fn keyword)))
                                         jwks-edn->public-keys)})))
                                         

(defn- fetch-keys
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
89 90 91
  "Fetches the jwks from the supplied jwks-url and converts to java Keys.
  Returns a map keyed on key-id where each value is a RSAPublicKey object"
  [jwks-url]
92 93 94 95 96
  (try  (->> jwks-url
             slurp
             (#(json/read-str % :key-fn keyword))
             jwks-edn->public-keys)
        (catch Exception e false)))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
97 98 99 100 101 102 103 104


(def public-keys
    "Atom to hold the public keys used for signature validation in memory for
    caching purposes. The atom holds a clojure map with kid -> PublicKey pairs."
    (atom {}))


105 106 107 108
(s/fdef resolve-key
        :args (s/cat :jwks-url ::jwks-url
                     :jwt-header ::jwt-header)
        :ret  ::RSAPublicKey)
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
109 110 111 112 113

(defn resolve-key
  "Returns java.security.PublicKey given jwks-url and :kid in jwt-header.
  If no key is found refreshes"
  [jwks-url jwt-header]
114
  (let [key-fn (fn [] (get @public-keys (:kid jwt-header)))]
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
115 116
    (if-let [key (key-fn)]
      key
117
      (do (reset! public-keys (or (fetch-keys jwks-url) @public-keys))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
118 119
          (if-let [key (key-fn)]
            key
120
            (throw (ex-info (str "Could not locate public key corresponding to jwt header's kid: " (:kid jwt-header))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
121 122 123 124 125 126 127 128 129 130 131
                            {:type :validation :cause :unknown-key})))))))


(defn unsign
  "Given token, jwks-url, and optionally opts validates and returns the claims
  of the given json web token. Opts are the same as buddy-sign.jwt/unsign."
  ([token jwks-url]
   (unsign token jwks-url {})) 
  ([token jwks-url opts]
   (jwt/unsign token (partial resolve-key jwks-url) (merge {:alg :rs256} opts))))