diff --git a/.gitignore b/.gitignore index c53038ec0e3d3094216b6a0fed8967e71cad6f5d..18fe556acaeffeffdb632b5dc1846a9988fb0db3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ pom.xml.asc /.nrepl-port .hgignore .hg/ +.cpcache \ No newline at end of file diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000000000000000000000000000000000000..49cc9243043862c893ebfa02d3828d2863ce5ffc --- /dev/null +++ b/deps.edn @@ -0,0 +1,7 @@ +{:paths ["src"] + :deps {org.clojure/clojure {:mvn/version "1.9.0"} + buddy/buddy-core {:mvn/version "1.5.0"} + buddy/buddy-sign {:mvn/version "3.0.0"} + org.clojure/data.json {:mvn/version "0.2.6"} + org.clojure/algo.generic {:mvn/version "0.1.3"} + invetica/uri {:mvn/version "0.5.0"}}} diff --git a/project.clj b/project.clj index 9eb565c5fd656145b7ad8194c4e5710e788638d7..0a8ffadfb6a83e8911dc570449c0b6828dcd93d4 100644 --- a/project.clj +++ b/project.clj @@ -3,12 +3,9 @@ :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.9.0"] - [buddy/buddy-core "1.5.0"] - [buddy/buddy-sign "3.0.0"] - [org.clojure/data.json "0.2.6"] - [org.clojure/algo.generic "0.1.3"] - [invetica/uri "0.5.0"]] + :middleware [lein-tools-deps.plugin/resolve-dependencies-with-deps-edn] + :plugins [[lein-tools-deps "0.4.1"]] + :lein-tools-deps/config {:config-files [:project]} :profiles {:test {:resource-paths ["test-resources"] :dependencies [[org.clojure/test.check "0.9.0"] [clj-time "0.14.4"] diff --git a/src/no/nsd/clj_jwt.clj b/src/no/nsd/clj_jwt.clj index 8b651043d49b2ffe316f4d5c3f99c42f60e3cce9..fe842e4f3c395a69878e9996c6795580a5211f94 100644 --- a/src/no/nsd/clj_jwt.clj +++ b/src/no/nsd/clj_jwt.clj @@ -12,20 +12,21 @@ (def jwtregex #"^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?$") -(s/def ::sub (s/nilable :no.nsd.authorizer/uuid)) +(s/def ::sub (s/nilable string?)) -(s/def ::kid string?) +(s/def ::kid (s/with-gen (s/nilable string?) + #(s/gen #{"test-key"}))) (s/def ::scope (s/nilable string?)) (s/def ::scopes (s/nilable (s/coll-of string? :kind set?))) -(s/def ::exp (s/and integer? - pos?)) +(s/def ::exp (s/nilable (s/and integer? + pos?))) -(s/def ::kty (s/with-gen (s/and string? - #(= "RSA" %)) - #(s/gen #{"RSA"}))) +(s/def ::kty (s/with-gen (s/nilable (s/and string? + #(= "RSA" %))) + #(s/gen #{"RSA" nil}))) (s/def ::n (s/with-gen string? #(s/gen #{;;valid: @@ -35,13 +36,16 @@ (s/def ::e string?) -(s/def ::subject (s/nilable (s/keys :req-un [::sub ::exp] - :opt-un [::scope - ::scopes]))) +(s/def ::claims (s/nilable (s/keys :opt-un [::exp + ::scope + ::scopes + ::sub]))) (s/def ::jwt (s/nilable (s/and string? #(re-matches jwtregex %)))) +(s/def ::jwt-header (s/keys :req-un [::kid ::kty])) + (s/def ::jwk (s/keys :req-un [::kty ::e ::n ::kid])) (s/def ::RSAPublicKey keys/public-key?) @@ -53,12 +57,15 @@ #(s/gen #{(resource "jwks.json") (resource "jwks-other.json")}))) +(s/def ::jwks-url (s/or :url :invetica.uri/absolute-uri + :resource ::resource)) + (s/fdef jwks-edn->public-keys :args (s/cat :jwks (s/coll-of ::jwk :type vector?)) :ret ::key-store) -(defn jwks-edn->public-keys +(defn- jwks-edn->public-keys "Transform vector of json-web-keys to map of kid -> PublicKey pairs." [json-web-keys] (->> json-web-keys @@ -70,18 +77,23 @@ (s/fdef fetch-keys - :args (s/cat :jwks-url (s/or :url :invetica.uri/absolute-uri - :resource ::resource)) - :ret ::key-store) - -(defn fetch-keys + :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 "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] - (->> jwks-url - slurp - (#(json/read-str % :key-fn keyword)) - jwks-edn->public-keys)) + (try (->> jwks-url + slurp + (#(json/read-str % :key-fn keyword)) + jwks-edn->public-keys) + (catch Exception e false))) (def public-keys @@ -90,24 +102,22 @@ (atom {})) -(defn refresh-keys - "Fetches keys and updates key store (atom)" - [jwks-url] - (reset! public-keys - (fetch-keys jwks-url))) - +(s/fdef resolve-key + :args (s/cat :jwks-url ::jwks-url + :jwt-header ::jwt-header) + :ret ::RSAPublicKey) (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] - (let [key-fn (fn [] (get (deref public-keys) (:kid jwt-header)))] + (let [key-fn (fn [] (get @public-keys (:kid jwt-header)))] (if-let [key (key-fn)] key - (do (reset! public-keys (fetch-keys jwks-url)) + (do (reset! public-keys (or (fetch-keys jwks-url) @public-keys)) (if-let [key (key-fn)] key - (throw (ex-info "Could not locate public key corresponding to jwt header's kid." + (throw (ex-info (str "Could not locate public key corresponding to jwt header's kid: " (:kid jwt-header)) {:type :validation :cause :unknown-key}))))))) diff --git a/test/no/nsd/clj_jwt_test.clj b/test/no/nsd/clj_jwt_test.clj index 7077cd40d76619192423ba98a1cc3a0b28e13b09..c81b8b0ffb12a0e873e3a084962a37330e508754 100644 --- a/test/no/nsd/clj_jwt_test.clj +++ b/test/no/nsd/clj_jwt_test.clj @@ -6,7 +6,6 @@ [clojure.java.io :refer [resource]] [clojure.test :refer [deftest testing is]])) - (def example-jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c") @@ -54,14 +53,6 @@ vLu9XxKFHYlWPccluz3pqDfaGNPO12968DAldwvAV6hTGgx7oMaNPu0UltgD/aaj (is (nil? (re-matches clj-jwt/jwtregex "ab12356723cdb.1235412513"))))) -(deftest refresh-public-keys! - (testing "Repopulates the keystore" - (is (with-redefs [clj-jwt/public-keys (atom {}) - clj-jwt/fetch-keys (fn [x] (identity {:foo :bar}))] - (not (empty? (do (clj-jwt/refresh-keys "") - @clj-jwt/public-keys))))))) - - (deftest unsign-jwt (testing "Unsigns jwt and returns payload" (is (= (with-redefs [clj-jwt/public-keys (atom {"test-key" ec-pubkey})] diff --git a/test/no/nsd/exerciser.clj b/test/no/nsd/exerciser.clj index eb5943df5f91f065132aeb2f3a0b3ed7f069ce15..5f8e73d69dfee74853101655e43e0e96316127c8 100644 --- a/test/no/nsd/exerciser.clj +++ b/test/no/nsd/exerciser.clj @@ -44,7 +44,9 @@ vLu9XxKFHYlWPccluz3pqDfaGNPO12968DAldwvAV6hTGgx7oMaNPu0UltgD/aaj (def sample-claims {:sub "f750bd26-ae85-4808-8f9a-dcc964fc8664" :exp (time/plus (time/now) (time/minutes 30))}) -(def untestable-funs ['no.nsd.clj-jwt/fetch-keys]) +(def untestable-funs ['no.nsd.clj-jwt/fetch-keys + 'no.nsd.clj-jwt/resolve-key + 'no.nsd.clj-jwt/unsign]) (defn generate-jwt [claims key]