clj_jwt.clj 5.61 KB
Newer Older
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
1 2 3 4 5 6 7 8 9
(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]
10
            [clojure.tools.logging :as log]
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
11 12 13 14 15
            [invetica.uri :as uri]))


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

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

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

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

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

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

28 29 30
(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
31 32 33 34 35 36 37 38 39

(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?)

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

Kjetil Thuen's avatar
Kjetil Thuen committed
45 46 47 48
(s/def ::jwt (s/nilable (s/with-gen (s/and string?
                                           #(re-matches jwtregex %))
                          #(s/gen #{(jwt/sign (gen/generate (s/gen ::claims))
                                              "secret")}))))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
49

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

Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
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 %)
60 61 62
                    ;; Always use local resources to avoid spamming actual servers
                    #(s/gen #{(resource "jwks.json")
                              (resource "jwks-other.json")})))
63

64 65 66 67 68
(s/def ::jwks-url (s/with-gen (s/or :url  :invetica.uri/absolute-uri
                                          :resource ::resource)
                              ;; Always use local resources to avoid spamming actual servers
                              #(s/gen #{(resource "jwks.json")
                                        (resource "jwks-other.json")})))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
69 70 71 72 73

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

74
(defn- jwks-edn->public-keys
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
75 76 77 78 79 80 81 82 83 84 85
  "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
Kjetil Thuen's avatar
Kjetil Thuen committed
86 87 88 89 90 91 92
  :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)})))

93 94

(defn- fetch-keys
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
95 96 97
  "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]
98
  (log/info "Fetching keys from jwks-url" jwks-url)
99 100 101 102
  (try  (->> jwks-url
             slurp
             (#(json/read-str % :key-fn keyword))
             jwks-edn->public-keys)
103 104
        (catch Exception e (do (log/error "Could not fetch jwks keys")
                               false))))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
105 106 107 108 109 110 111

(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 {}))


112
(s/fdef resolve-key
113 114
        :args (s/cat :jwks-url    ::jwks-url
                     :jwt-header  ::jwt-header)
115
        :ret  ::RSAPublicKey)
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
116 117 118 119 120

(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]
121
  (log/debug "Resolving key " jwt-header " from " jwks-url)
122
  (let [key-fn (fn [] (get @public-keys (:kid jwt-header)))]
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
123 124
    (if-let [key (key-fn)]
      key
125 126
      (do (log/info "Retry resolve key " jwt-header " from " jwks-url) 
          (reset! public-keys (or (fetch-keys jwks-url) @public-keys))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
127 128
          (if-let [key (key-fn)]
            key
129 130 131 132
            (do
              (log/error "Could not locate public key corresponding to jwt header's kid: " (:kid jwt-header))
              (throw (ex-info (str "Could not locate public key corresponding to jwt header's kid: " (:kid jwt-header))
                              {:type :validation :cause :unknown-key}))))))))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
133 134


135 136 137 138 139
(s/fdef unsign
        :args (s/cat :token ::jwt
                     :jwks-url ::jwks-url)
        :ret  ::claims)

Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
140 141 142 143
(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]
Kjetil Thuen's avatar
Kjetil Thuen committed
144
   (unsign token jwks-url {}))
Snorre Magnus Davøen's avatar
Snorre Magnus Davøen committed
145 146
  ([token jwks-url opts]
   (jwt/unsign token (partial resolve-key jwks-url) (merge {:alg :rs256} opts))))