From 6d6b8c907d6c3c37fcb1cfdaaa88b6f8c2df64f2 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 13 Jun 2024 17:35:36 -0400 Subject: [PATCH 01/45] Add simple Java wrapper --- project.clj | 3 +- src/com/mdsol/mauth/clojure/client.clj | 151 +++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/com/mdsol/mauth/clojure/client.clj diff --git a/project.clj b/project.clj index c86838e..5c85f81 100644 --- a/project.clj +++ b/project.clj @@ -11,7 +11,8 @@ [http-kit "2.4.0-alpha2"] [clj-http "3.13.0"] [org.clojure/data.json "2.5.0"] - [javax.xml.bind/jaxb-api "2.3.1"]] + [javax.xml.bind/jaxb-api "2.3.1"] + [com.mdsol/mauth-signer "16.0.0"]] :deploy-repositories [["releases" {:url "https://clojars.org/repo" diff --git a/src/com/mdsol/mauth/clojure/client.clj b/src/com/mdsol/mauth/clojure/client.clj new file mode 100644 index 0000000..e101a21 --- /dev/null +++ b/src/com/mdsol/mauth/clojure/client.clj @@ -0,0 +1,151 @@ +(ns com.mdsol.mauth.clojure.client + (:require [clojure.string :as str]) + (:import (clojure.lang IFn Keyword) + (com.mdsol.mauth DefaultSigner MAuthVersion Signer) + (com.mdsol.mauth.util CurrentEpochTimeProvider EpochTimeProvider) + (java.io ByteArrayInputStream CharArrayReader InputStream StringReader) + (java.util List UUID))) + +(set! *warn-on-reflection* true) + +(defmulti ->uuid-impl type) +(defmethod ->uuid-impl UUID [x] + x) +(defmethod ->uuid-impl String [x] + (parse-uuid x)) +(defmethod ->uuid-impl :default [x] + (->uuid-impl (str x))) + +(defn ->uuid ^UUID [x] + (->uuid-impl x)) + +(defmulti ->version-impl type) +(defmethod ->version-impl MAuthVersion [x] + x) +(defmethod ->version-impl String [x] + (MAuthVersion/valueOf (str/upper-case x))) +(defmethod ->version-impl :default [x] + (->version-impl (str x))) +(defmethod ->version-impl Keyword [x] + (->version-impl (name x))) + +(defn ->version ^MAuthVersion [x] + (->version-impl x)) + +(defmulti ->epoch-time-provider-impl type) +(defmethod ->epoch-time-provider-impl EpochTimeProvider [x] + x) +(defmethod ->epoch-time-provider-impl IFn [x] + (reify EpochTimeProvider + (inSeconds [_this] + (long (x))))) + +(defn ->epoch-time-provider ^EpochTimeProvider [x] + (->epoch-time-provider-impl x)) + +(def current-epoch-time-provider (CurrentEpochTimeProvider.)) + +(defn default-signer [{:keys [app-uuid private-key + epoch-time-provider sign-versions] + :or {epoch-time-provider current-epoch-time-provider + sign-versions [:MWSV2]}}] + (DefaultSigner. (->uuid app-uuid) + ^String private-key + (->epoch-time-provider epoch-time-provider) + ^List (list* (map ->version sign-versions)))) + +(comment + (def signer + (default-signer {:sign-versions [:MWS :MWSV2] + :app-uuid (random-uuid) + ;; This key was generated specifically for testing + :private-key "-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA62E+E3/I+rH9PZ9Z7GwUakZeDbBqBj52sfFyt2M4LBawPojp +DBtAHOGxhCff8yncoDa8QRKTqVwn7kiTcjsuKub40/JMbnY1ltjaRm473vhMiUZm +q1c5jFyU24dEZ7DYFpC1bTpjyCeI6dgzfcqDaO366/fpuMGURGgVOQ7Fi71FKpYc +B5cZp8ywgQjSOfpM9u0e0yLukfSvDxeDGGpV1JnD9xcrojJI2iyFe2i+LPESpp6L +YHKKHilBbK3cI7uTfiIvdsYc1SLYZQtyNXDCBbvJ/zgcSTteh8D9sioL/2uLHKH3 +2VRlDn0zafulP/G1pYyT89hVE1kj6P5A0m6JMwIDAQABAoIBAQDPcALCMoLQFV6m +GTKpvlJ7moo3LDs0R4ZZqf08i2+sw04NvqEL71QgX/MPEgk3BrSOac6c1p9VyYbT +ZBi1ulwkqSuvtkEPtJPj3jb3jRysk0z4Shgfdp6cCdeSZPKvI1Y9BMkvex8G/XiX +BdfTS09mgRz7KqBLGCbv5n6Vq2QDklidfrSJY8fvjzyGVZbMLUvMlFOWsA1vZQfZ +k0sKxqwgpbyoBFKsRp9CTMW6dsCEJGNFFfjvB2Eu1J5g7NN/1E4b/i4tyaDYahW4 +8SeXTnm3Jr8JmfH99FGZ5CWV1Y8Kamo+AXUFdeVvwYEJJuepVxwOwisCJYvYy9IN +oV5qtnrxAoGBAPbkfqf0cCDh5wpiIfL/KwmhBdmes3fbpHF7kfNFtmBJL5RlyqsW +sNwjwLw3LEQKSmTkufZ0ozujobgtIAdRsZvzEyymVNC+YszkVgwPgKI9we+w9dHj +HKUR+2zyxoH6/FpxxzyIwXie5i+7k3Mshml9j5nX7GPJHU+i76fkkKv5AoGBAPQQ +B1ZTCfXj9ygH3ut0iUs5yK+wF0/1QV+Qu67Cub5q+ZSNGx0aKzHxuLNsPZk7uJDg +4gYvjFdq81f0Gv3JMDkULClxQif+9kUVXt9bpQbJ6S+tCCTtm2wOh6JjdN/phgAk ++wxFsDsW+DDARi2UkgILEglDDXJhi4PPm+6TSrGLAoGBAL7TMMnj9l6T/B1cZ90H +OF6C5KClNxWm4F0OI2qiMSoOpwXN/21pZl1gDPHsuvwD8Cg3WTySPjA0cySFTEIb +9GkS4XkbPmbxIDA5NACyYrwDe8glQHpvTY6QbYJxythgA3hshI/XK6JtPoEaPAdD +HMigUcOYzo75vPv2dcGQufkRAoGBAOHK3m7fngxtnd/cAEFG7Cm7SM45FCg2Fwfp +p6kTn7Hp2AK11MrExgeLwLvFvOtB1Au88X6ViLnrSTwqqrn14nY8Ems4y+Kiv4XE +MqRjbbZtIB2qcClx5WM/wf3bE2p/6ifCDrwY0OSp6G15xLMwiy/2u/XzocIbOm50 +qKc8f1LnAoGBAMEBKHFEKvpQ/00RKT1VJkpUHk3SoOVey8PpaTbNFlggz/2FcO5i +JXNpe60qgURHJigvYmseF9p7f36w2cnGMpJowHhbY7QFYosuIOQ7Am8h24dgpHtd +B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT +-----END RSA PRIVATE KEY-----"}))) + +;; The JVM does not have union types, so this is the best we can do +(defmulti ->array-or-input-stream type) + +;; Lazy reading +(defmethod ->array-or-input-stream InputStream [x] + [:input-stream x]) + +;; Eager reading +(defmethod ->array-or-input-stream (type (byte-array 0)) [x] + [:array x]) +(defmethod ->array-or-input-stream String [^String x] + (->array-or-input-stream (.getBytes x "UTF-8"))) +(defmethod ->array-or-input-stream CharSequence [x] + (->array-or-input-stream (str x))) +(defmethod ->array-or-input-stream ByteArrayInputStream + [^ByteArrayInputStream x] + (->array-or-input-stream (.readAllBytes x))) +(defmethod ->array-or-input-stream StringReader [^StringReader x] + (->array-or-input-stream (slurp x))) +(defmethod ->array-or-input-stream CharArrayReader [^CharArrayReader x] + (->array-or-input-stream (slurp x))) + +(defn gen-req-headers [^Signer signer + {:keys [request-method body ^String uri + ^String query-string]}] + (let [method (if (ident? request-method) + (name request-method) + ^String request-method) + [t b] (->array-or-input-stream body)] + (into {} + ;; The InputStream overload can only provide signatures for one + ;; version at a time, while the array overload requires holding the + ;; whole request body in memory. Neither fits all use cases, so we + ;; dispatch to one or the other depending on whether the request body + ;; is already fully held in memory. + (case t + :input-stream (.generateRequestHeaders signer method uri + ^InputStream b + query-string) + :array (.generateRequestHeaders signer method uri + ^bytes b + query-string))))) + +(comment + (gen-req-headers signer {:request-method :post + :uri "/foo" + :body "Hey hey"}) + ) + +(defn wrap-client [signer client] + (fn + ([req] + (client (update req :headers merge (gen-req-headers signer req)))) + ([req respond raise] + (client (update req :headers merge (gen-req-headers signer req)) + respond raise)))) + +(comment + ((wrap-client signer prn) {:request-method :post + :headers {"content-type" "whatever"} + :uri "/foo" + :body "Hey hey"})) From 1b40a816b800a11243a9ef3011d16ad1464590b0 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:45:57 -0400 Subject: [PATCH 02/45] Add mauth test suite as submodule --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ff92868 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mauth-protocol-test-suite"] + path = mauth-protocol-test-suite + url = git@github.com:mdsol/mauth-protocol-test-suite.git From 09815b0718906456ae935406cd5f19d2a8ed9b5b Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:47:01 -0400 Subject: [PATCH 03/45] Formatting --- project.clj | 26 +++++++++++++------------- src/com/mdsol/mauth/clojure/client.clj | 3 +-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/project.clj b/project.clj index 5c85f81..ce713c6 100644 --- a/project.clj +++ b/project.clj @@ -27,23 +27,23 @@ ["vcs" "push"]] :aliases {"bump!" ^{:doc "Bump the project version number and push the commits to the original repository."} - ["do" - ["vcs" "assert-committed"] - ["change" "version" "leiningen.release/bump-version"] - ["vcs" "commit"] - ["vcs" "push"]]} + ["do" + ["vcs" "assert-committed"] + ["change" "version" "leiningen.release/bump-version"] + ["vcs" "commit"] + ["vcs" "push"]]} :target-path "target/%s" :profiles {:uberjar {:aot :all :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}} :jvm-opts ~(concat - [] ;other opts... - (if (let [v (-> (System/getProperty "java.version") - (clojure.string/split #"[.]") - first - Integer.)] - (and (>= v 9) (< v 11))) - ["--add-modules" "java.xml.bind"] - [])) + [] ;other opts... + (if (let [v (-> (System/getProperty "java.version") + (clojure.string/split #"[.]") + first + Integer.)] + (and (>= v 9) (< v 11))) + ["--add-modules" "java.xml.bind"] + [])) :aot :all) diff --git a/src/com/mdsol/mauth/clojure/client.clj b/src/com/mdsol/mauth/clojure/client.clj index e101a21..918a79a 100644 --- a/src/com/mdsol/mauth/clojure/client.clj +++ b/src/com/mdsol/mauth/clojure/client.clj @@ -133,8 +133,7 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT (comment (gen-req-headers signer {:request-method :post :uri "/foo" - :body "Hey hey"}) - ) + :body "Hey hey"})) (defn wrap-client [signer client] (fn From 3d607f6c9c6e0db0d94f25ab521cb8311c4561e2 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:48:42 -0400 Subject: [PATCH 04/45] Remove AOT uberjar Libraries need not and should not build AOT or build an uberjar. --- project.clj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/project.clj b/project.clj index ce713c6..6e8c01f 100644 --- a/project.clj +++ b/project.clj @@ -34,8 +34,6 @@ ["vcs" "push"]]} :target-path "target/%s" - :profiles {:uberjar {:aot :all - :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}} :jvm-opts ~(concat [] ;other opts... @@ -45,5 +43,4 @@ Integer.)] (and (>= v 9) (< v 11))) ["--add-modules" "java.xml.bind"] - [])) - :aot :all) + []))) From f036e007dde5e2e4a95351a91fbc4725c7c82069 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:49:09 -0400 Subject: [PATCH 05/45] Add enterprise maven repository --- project.clj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/project.clj b/project.clj index 6e8c01f..0f1e2f9 100644 --- a/project.clj +++ b/project.clj @@ -14,6 +14,10 @@ [javax.xml.bind/jaxb-api "2.3.1"] [com.mdsol/mauth-signer "16.0.0"]] + :repositories [["maven-prod-virtual" {:url "https://mdsol.jfrog.io/mdsol/maven-prod-virtual" + :username :env/artifactory_username + :password :env/artifactory_password}]] + :deploy-repositories [["releases" {:url "https://clojars.org/repo" :sign-releases false From 2e6068b75d642ba0fe844ea5d2e848c1ae0430ae Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:49:26 -0400 Subject: [PATCH 06/45] Add clj-kondo config --- .clj-kondo/config.edn | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .clj-kondo/config.edn diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn new file mode 100644 index 0000000..3beb05b --- /dev/null +++ b/.clj-kondo/config.edn @@ -0,0 +1,28 @@ +{:linters {:aliased-namespace-symbol {:level :warning} + :case-symbol-test {:level :warning} + :condition-always-true {:level :warning} + :bb.edn-task-missing-docstring {:level :warning} + :docstring-no-summary {:level :warning} + :docstring-leading-trailing-whitespace {:level :warning} + :dynamic-var-not-earmuffed {:level :warning} + :equals-false {:level :warning} + :equals-true {:level :warning} + :def-fn {:level :warning} + :reduce-without-init {:level :warning} + :keyword-binding {:level :warning} + :main-without-gen-class {:level :error} + :minus-one {:level :warning} + :missing-docstring {:level :warning} + :plus-one {:level :warning} + :redundant-fn-wrapper {:level :warning} + :redundant-call {:level :warning} + :refer {:level :warning + :exclude [clojure.test]} + :single-key-in {:level :warning} + :shadowed-var {:level :warning} + :unused-alias {:level :warning} + :used-underscored-binding {:level :warning} + :unsorted-imports {:level :warning} + :unsorted-required-namespaces {:level :warning} + :warn-on-reflection {:level :warning} + :unresolved-namespace {:level :warning}}} From cefd645b82bf1fd6ee8f94ba242cb179e5484ab9 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:54:25 -0400 Subject: [PATCH 07/45] Rename ns to match other lib conventions --- src/com/mdsol/mauth/clojure/{client.clj => signer.clj} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/com/mdsol/mauth/clojure/{client.clj => signer.clj} (99%) diff --git a/src/com/mdsol/mauth/clojure/client.clj b/src/com/mdsol/mauth/clojure/signer.clj similarity index 99% rename from src/com/mdsol/mauth/clojure/client.clj rename to src/com/mdsol/mauth/clojure/signer.clj index 918a79a..f998e9f 100644 --- a/src/com/mdsol/mauth/clojure/client.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -1,4 +1,4 @@ -(ns com.mdsol.mauth.clojure.client +(ns com.mdsol.mauth.clojure.signer (:require [clojure.string :as str]) (:import (clojure.lang IFn Keyword) (com.mdsol.mauth DefaultSigner MAuthVersion Signer) From df29dcd191b0cce50b39ef0c0aeb34c15c92ff7c Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:02:52 -0400 Subject: [PATCH 08/45] Add docstrings --- src/com/mdsol/mauth/clojure/signer.clj | 86 +++++++++++++++++++++----- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index f998e9f..db85b20 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -8,7 +8,11 @@ (set! *warn-on-reflection* true) -(defmulti ->uuid-impl type) +(defmulti ->uuid-impl + "Multimethod to convert an arbitrary type to `UUID`. + This multimethod underlies the `->uuid` function, which simply adds the + appropriate return type hint." + type) (defmethod ->uuid-impl UUID [x] x) (defmethod ->uuid-impl String [x] @@ -16,10 +20,18 @@ (defmethod ->uuid-impl :default [x] (->uuid-impl (str x))) -(defn ->uuid ^UUID [x] +(defn ->uuid + "Converts an arbitrary type to `UUID`. + To extend support to additional types, define a new method for + `->uuid-impl`." + ^UUID [x] (->uuid-impl x)) -(defmulti ->version-impl type) +(defmulti ->version-impl + "Multimethod to convert an arbitrary type to `MAuthVersion`. + This multimethod underlies the `->version` function, which simply adds the + appropriate return type hint." + type) (defmethod ->version-impl MAuthVersion [x] x) (defmethod ->version-impl String [x] @@ -29,10 +41,18 @@ (defmethod ->version-impl Keyword [x] (->version-impl (name x))) -(defn ->version ^MAuthVersion [x] +(defn ->version + "Converts an arbitrary type to `MAuthVersion`. + To extend support to additional types, define a new method for + `->version-impl`." + ^MAuthVersion [x] (->version-impl x)) -(defmulti ->epoch-time-provider-impl type) +(defmulti ->epoch-time-provider-impl + "Multimethod to convert an arbitrary type to `EpochTimeProvider`. + This multimethod underlies the `->epoch-time-provider` function, which simply + adds the appropriate return type hint." + type) (defmethod ->epoch-time-provider-impl EpochTimeProvider [x] x) (defmethod ->epoch-time-provider-impl IFn [x] @@ -40,15 +60,38 @@ (inSeconds [_this] (long (x))))) -(defn ->epoch-time-provider ^EpochTimeProvider [x] +(defn ->epoch-time-provider + "Converts an arbitrary type to `EpochTimeProvider`. + To extend support to additional types, define a new method for + `->epoch-time-provider-impl`." + ^EpochTimeProvider [x] (->epoch-time-provider-impl x)) -(def current-epoch-time-provider (CurrentEpochTimeProvider.)) - -(defn default-signer [{:keys [app-uuid private-key - epoch-time-provider sign-versions] - :or {epoch-time-provider current-epoch-time-provider - sign-versions [:MWSV2]}}] +(def current-epoch-time-provider + "Provides the actual current time according to the system clock." + (CurrentEpochTimeProvider.)) + +(defn default-signer + "Returns a signer capable of producing MAuth signatures. + + Required arguments: + - app-uuid: The UUID registered in the MAuth server for the signing + application. + - private-key: The application's private key as a String. + Optional arguments: + - epoch-time-provider: A function which returns the current time as seconds + since the Unix epoch. Defaults to a function which returns the system clock + time. + - sign-versions: A collection of MAuth versions for which signatures should + be produced. Defaults to `[:MWSV2]`. + + The types for all of these arguments are flexible. Support for new types can + be added by installing new methods for the multimethods defined in this + namespace." + [{:keys [app-uuid private-key + epoch-time-provider sign-versions] + :or {epoch-time-provider current-epoch-time-provider + sign-versions [:MWSV2]}}] (DefaultSigner. (->uuid app-uuid) ^String private-key (->epoch-time-provider epoch-time-provider) @@ -88,7 +131,12 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT -----END RSA PRIVATE KEY-----"}))) ;; The JVM does not have union types, so this is the best we can do -(defmulti ->array-or-input-stream type) +(defmulti ->array-or-input-stream + "Converts an arbitrary type to either `byte[]` or `InputStream`. + The return value is a vector whose first element is either `:array` or + `:input-stream`, and whose second element is a value of the corresponding + type." + type) ;; Lazy reading (defmethod ->array-or-input-stream InputStream [x] @@ -109,9 +157,10 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT (defmethod ->array-or-input-stream CharArrayReader [^CharArrayReader x] (->array-or-input-stream (slurp x))) -(defn gen-req-headers [^Signer signer - {:keys [request-method body ^String uri - ^String query-string]}] +(defn gen-req-headers + "Given a signer and a Ring request, returns a map of MAuth headers." + [^Signer signer + {:keys [request-method body ^String uri ^String query-string]}] (let [method (if (ident? request-method) (name request-method) ^String request-method) @@ -135,7 +184,10 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT :uri "/foo" :body "Hey hey"})) -(defn wrap-client [signer client] +(defn wrap-client + "Middleware for clients conforming to the Ring/clj-http signature. + Adds MAuth headers to requests." + [client signer] (fn ([req] (client (update req :headers merge (gen-req-headers signer req)))) From 85d2726eb7bec3653c24b6fc00ec98f25c00ff7f Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:04:21 -0400 Subject: [PATCH 09/45] Formatting --- src/com/mdsol/mauth/clojure/signer.clj | 63 +++++++++++++------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index db85b20..a4ccf72 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -1,9 +1,14 @@ (ns com.mdsol.mauth.clojure.signer (:require [clojure.string :as str]) (:import (clojure.lang IFn Keyword) - (com.mdsol.mauth DefaultSigner MAuthVersion Signer) + (com.mdsol.mauth DefaultSigner + MAuthVersion + Signer) (com.mdsol.mauth.util CurrentEpochTimeProvider EpochTimeProvider) - (java.io ByteArrayInputStream CharArrayReader InputStream StringReader) + (java.io ByteArrayInputStream + CharArrayReader + InputStream + StringReader) (java.util List UUID))) (set! *warn-on-reflection* true) @@ -13,12 +18,9 @@ This multimethod underlies the `->uuid` function, which simply adds the appropriate return type hint." type) -(defmethod ->uuid-impl UUID [x] - x) -(defmethod ->uuid-impl String [x] - (parse-uuid x)) -(defmethod ->uuid-impl :default [x] - (->uuid-impl (str x))) +(defmethod ->uuid-impl UUID [x] x) +(defmethod ->uuid-impl String [x] (parse-uuid x)) +(defmethod ->uuid-impl :default [x] (->uuid-impl (str x))) (defn ->uuid "Converts an arbitrary type to `UUID`. @@ -32,14 +34,10 @@ This multimethod underlies the `->version` function, which simply adds the appropriate return type hint." type) -(defmethod ->version-impl MAuthVersion [x] - x) -(defmethod ->version-impl String [x] - (MAuthVersion/valueOf (str/upper-case x))) -(defmethod ->version-impl :default [x] - (->version-impl (str x))) -(defmethod ->version-impl Keyword [x] - (->version-impl (name x))) +(defmethod ->version-impl MAuthVersion [x] x) +(defmethod ->version-impl String [x] (MAuthVersion/valueOf (str/upper-case x))) +(defmethod ->version-impl :default [x] (->version-impl (str x))) +(defmethod ->version-impl Keyword [x] (->version-impl (name x))) (defn ->version "Converts an arbitrary type to `MAuthVersion`. @@ -53,8 +51,7 @@ This multimethod underlies the `->epoch-time-provider` function, which simply adds the appropriate return type hint." type) -(defmethod ->epoch-time-provider-impl EpochTimeProvider [x] - x) +(defmethod ->epoch-time-provider-impl EpochTimeProvider [x] x) (defmethod ->epoch-time-provider-impl IFn [x] (reify EpochTimeProvider (inSeconds [_this] @@ -83,23 +80,23 @@ since the Unix epoch. Defaults to a function which returns the system clock time. - sign-versions: A collection of MAuth versions for which signatures should - be produced. Defaults to `[:MWSV2]`. + be produced. Defaults to `[:mwsv2]`. The types for all of these arguments are flexible. Support for new types can be added by installing new methods for the multimethods defined in this namespace." - [{:keys [app-uuid private-key - epoch-time-provider sign-versions] - :or {epoch-time-provider current-epoch-time-provider - sign-versions [:MWSV2]}}] + [& {:keys [app-uuid private-key + epoch-time-provider sign-versions] + :or {epoch-time-provider current-epoch-time-provider + sign-versions [:mwsv2]}}] (DefaultSigner. (->uuid app-uuid) ^String private-key (->epoch-time-provider epoch-time-provider) ^List (list* (map ->version sign-versions)))) (comment - (def signer - (default-signer {:sign-versions [:MWS :MWSV2] + (def my-signer + (default-signer {:sign-versions [:mws :mwsv2] :app-uuid (random-uuid) ;; This key was generated specifically for testing :private-key "-----BEGIN RSA PRIVATE KEY----- @@ -156,6 +153,8 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT (->array-or-input-stream (slurp x))) (defmethod ->array-or-input-stream CharArrayReader [^CharArrayReader x] (->array-or-input-stream (slurp x))) +(defmethod ->array-or-input-stream nil [_] + [:array nil]) (defn gen-req-headers "Given a signer and a Ring request, returns a map of MAuth headers." @@ -180,9 +179,9 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT query-string))))) (comment - (gen-req-headers signer {:request-method :post - :uri "/foo" - :body "Hey hey"})) + (gen-req-headers my-signer {:request-method :post + :uri "/foo" + :body "Hey hey"})) (defn wrap-client "Middleware for clients conforming to the Ring/clj-http signature. @@ -196,7 +195,7 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT respond raise)))) (comment - ((wrap-client signer prn) {:request-method :post - :headers {"content-type" "whatever"} - :uri "/foo" - :body "Hey hey"})) + ((wrap-client prn my-signer) {:request-method :post + :headers {"content-type" "whatever"} + :uri "/foo" + :body "Hey hey"})) From 571ab78f453e8ff66a78bc3b025907c421149b78 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:04:40 -0400 Subject: [PATCH 10/45] Add suite-based tests --- project.clj | 5 +- test/com/mdsol/mauth/clojure/signer_test.clj | 77 ++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 test/com/mdsol/mauth/clojure/signer_test.clj diff --git a/project.clj b/project.clj index 0f1e2f9..8d24f5e 100644 --- a/project.clj +++ b/project.clj @@ -3,7 +3,10 @@ :url "https://github.com/mdsol/clojure-mauth-client" :license {:name "MIT" :url "https://opensource.org/licenses/MIT"} - :dependencies [[org.clojure/clojure "1.12.0"] + :dependencies [[camel-snake-kebab "0.4.3"] + [com.cnuernber/charred "1.033"] + [com.mdsol/mauth-test-utils "16.0.0+0-a6fb9a5f+20240725-1833-SNAPSHOT"] + [org.clojure/clojure "1.12.0"] [xsc/pem-reader "0.1.1"] [digest "1.4.10"] [org.clojure/data.codec "0.1.1"] diff --git a/test/com/mdsol/mauth/clojure/signer_test.clj b/test/com/mdsol/mauth/clojure/signer_test.clj new file mode 100644 index 0000000..b0c67b2 --- /dev/null +++ b/test/com/mdsol/mauth/clojure/signer_test.clj @@ -0,0 +1,77 @@ +(ns com.mdsol.mauth.clojure.signer-test + (:require [camel-snake-kebab.core :as csk] + [charred.api :as charred] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.test :refer [deftest is]] + [com.mdsol.mauth.clojure.signer :as sut]) + (:import (java.net URI) + (java.io File FilenameFilter))) + +(def suite-base (io/file "mauth-protocol-test-suite")) + +(defn child-by-ext [^File parent ext] + (first (.listFiles parent (reify FilenameFilter + (accept [_this _dir file-name] + (str/ends-with? file-name ext)))))) + +(defn ringify-request [{:keys [verb url body body-filepath]} path] + ;; Don't reinvent the wheel, just let Java parse the URI. + ;; It's a more complex task than you think. + (let [java-uri (URI. url)] + {:request-method verb + :uri (.getRawPath java-uri) + :query-string (.getRawQuery java-uri) + :body (or body + (when body-filepath + (io/input-stream (io/file path body-filepath))))})) + +(defn read-case [^File path] + {:name (.getName path) + :request (some-> (child-by-ext path ".req") + (charred/read-json :key-fn csk/->kebab-case-keyword) + (ringify-request path)) + ;; This implementation does not expose these intermediate steps in a testable + ;; way. That's fine, because they aren't part of the public contract anyway + ;; and don't really need to be tested directly. + #_#_:str-to-sign (some-> (child-by-ext path ".sts") + slurp) + #_#_:signature (some-> (child-by-ext path ".sig") + slurp) + :headers (some-> (child-by-ext path ".authz") + charred/read-json)}) + +(def signer + (let [{:keys [app-uuid request-time private-key-file]} + (charred/read-json (io/file suite-base "signing-config.json") + :key-fn csk/->kebab-case-keyword)] + (sut/default-signer :app-uuid app-uuid + :private-key (slurp (io/file suite-base private-key-file)) + :epoch-time-provider (constantly request-time)))) + +(def ignored-test-cases + #{;; In HTTP, foo//bar is not the same as foo/bar. This case is incorrect. + "get-normalize-multiple-slashes" + ;; This is invalid URL syntax. Query strings may not contain spaces. + "get-vanilla-query-space"}) + +(def test-cases + (->> (io/file suite-base "protocols" "MWSV2") + .listFiles + (remove #(ignored-test-cases (.getName ^File %))) + (map read-case))) + +(defn norm-headers [m] + (-> m + (update-keys str/lower-case) + (update-vals str) + vec)) + +;; Index-based iteration, because some requests are input streams, and they +;; cannot be used as literals for evaluation. +(doseq [i (range (count test-cases))] + (eval + `(deftest ~(-> test-cases (nth i) :name symbol) + (is (= ~(-> test-cases (nth i) :headers norm-headers) + (norm-headers (sut/gen-req-headers + signer (-> test-cases (nth ~i) :request)))))))) From 351da5b6f531c8c464d6fcb59a0a97e930a0e630 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:12:28 -0400 Subject: [PATCH 11/45] Major version bump --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 8d24f5e..b6d707a 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject clojure-mauth-client "2.0.9-SNAPSHOT" +(defproject clojure-mauth-client "3.0.0-SNAPSHOT" :description "Clojure Mauth Client" :url "https://github.com/mdsol/clojure-mauth-client" :license {:name "MIT" From 2ff00fddc831018faa3974bde789ceab583d21dc Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:12:46 -0400 Subject: [PATCH 12/45] Formatting --- test/com/mdsol/mauth/clojure/signer_test.clj | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_test.clj b/test/com/mdsol/mauth/clojure/signer_test.clj index b0c67b2..e5f28e8 100644 --- a/test/com/mdsol/mauth/clojure/signer_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_test.clj @@ -1,12 +1,14 @@ (ns com.mdsol.mauth.clojure.signer-test - (:require [camel-snake-kebab.core :as csk] - [charred.api :as charred] - [clojure.java.io :as io] - [clojure.string :as str] - [clojure.test :refer [deftest is]] - [com.mdsol.mauth.clojure.signer :as sut]) - (:import (java.net URI) - (java.io File FilenameFilter))) + (:require + [camel-snake-kebab.core :as csk] + [charred.api :as charred] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.test :refer [deftest is]] + [com.mdsol.mauth.clojure.signer :as sut]) + (:import + (java.io File FilenameFilter) + (java.net URI))) (def suite-base (io/file "mauth-protocol-test-suite")) From acc2d5cd36728e62c74762eb18d615604a4264a2 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:17:37 -0400 Subject: [PATCH 13/45] Move test dep to test profile --- project.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project.clj b/project.clj index b6d707a..1234b37 100644 --- a/project.clj +++ b/project.clj @@ -5,7 +5,6 @@ :url "https://opensource.org/licenses/MIT"} :dependencies [[camel-snake-kebab "0.4.3"] [com.cnuernber/charred "1.033"] - [com.mdsol/mauth-test-utils "16.0.0+0-a6fb9a5f+20240725-1833-SNAPSHOT"] [org.clojure/clojure "1.12.0"] [xsc/pem-reader "0.1.1"] [digest "1.4.10"] @@ -17,6 +16,8 @@ [javax.xml.bind/jaxb-api "2.3.1"] [com.mdsol/mauth-signer "16.0.0"]] + :profiles {:test {:dependencies [[com.mdsol/mauth-test-utils "16.0.0+0-a6fb9a5f+20240725-1833-SNAPSHOT"]]}} + :repositories [["maven-prod-virtual" {:url "https://mdsol.jfrog.io/mdsol/maven-prod-virtual" :username :env/artifactory_username :password :env/artifactory_password}]] From 71e6bd1a2dfa7010deb975aec183e2a31ce1a8a0 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:17:43 -0400 Subject: [PATCH 14/45] Change alias in test --- test/com/mdsol/mauth/clojure/signer_test.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_test.clj b/test/com/mdsol/mauth/clojure/signer_test.clj index e5f28e8..e212d96 100644 --- a/test/com/mdsol/mauth/clojure/signer_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_test.clj @@ -5,7 +5,7 @@ [clojure.java.io :as io] [clojure.string :as str] [clojure.test :refer [deftest is]] - [com.mdsol.mauth.clojure.signer :as sut]) + [com.mdsol.mauth.clojure.signer :as signer]) (:import (java.io File FilenameFilter) (java.net URI))) @@ -47,7 +47,7 @@ (let [{:keys [app-uuid request-time private-key-file]} (charred/read-json (io/file suite-base "signing-config.json") :key-fn csk/->kebab-case-keyword)] - (sut/default-signer :app-uuid app-uuid + (signer/default-signer :app-uuid app-uuid :private-key (slurp (io/file suite-base private-key-file)) :epoch-time-provider (constantly request-time)))) @@ -75,5 +75,5 @@ (eval `(deftest ~(-> test-cases (nth i) :name symbol) (is (= ~(-> test-cases (nth i) :headers norm-headers) - (norm-headers (sut/gen-req-headers + (norm-headers (signer/gen-req-headers signer (-> test-cases (nth ~i) :request)))))))) From dd9b84ee00425e6cc206364c426c32b0715d4d8a Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:18:05 -0400 Subject: [PATCH 15/45] Add warn-on-reflection --- test/com/mdsol/mauth/clojure/signer_test.clj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/com/mdsol/mauth/clojure/signer_test.clj b/test/com/mdsol/mauth/clojure/signer_test.clj index e212d96..ece4149 100644 --- a/test/com/mdsol/mauth/clojure/signer_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_test.clj @@ -10,6 +10,8 @@ (java.io File FilenameFilter) (java.net URI))) +(set! *warn-on-reflection* true) + (def suite-base (io/file "mauth-protocol-test-suite")) (defn child-by-ext [^File parent ext] From 6c5788a9c064a4ae8235e85ee4129bc216d04446 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 19:20:59 -0400 Subject: [PATCH 16/45] Extract conversions to separate namespace --- src/com/mdsol/mauth/clojure/convert.clj | 101 +++++++++++++++++++++ src/com/mdsol/mauth/clojure/signer.clj | 115 ++++-------------------- 2 files changed, 117 insertions(+), 99 deletions(-) create mode 100644 src/com/mdsol/mauth/clojure/convert.clj diff --git a/src/com/mdsol/mauth/clojure/convert.clj b/src/com/mdsol/mauth/clojure/convert.clj new file mode 100644 index 0000000..46c2fc4 --- /dev/null +++ b/src/com/mdsol/mauth/clojure/convert.clj @@ -0,0 +1,101 @@ +(ns com.mdsol.mauth.clojure.convert + (:require + [clojure.string :as str]) + (:import + (clojure.lang IFn Keyword) + (com.mdsol.mauth MAuthVersion) + (com.mdsol.mauth.util EpochTimeProvider) + (java.io + ByteArrayInputStream + CharArrayReader + InputStream + StringReader) + (java.util UUID))) + +(set! *warn-on-reflection* true) + +(defprotocol UUIDLike + (as-uuid [this])) + +(extend-protocol UUIDLike + UUID + (as-uuid [this] this) + String + (as-uuid [this] (parse-uuid this)) + Object + (as-uuid [this] (as-uuid (str this)))) + +(defn ->uuid + "Converts argument to a `UUID`. + To support additional types, extend the `UUIDLike` protocol." + ^UUID [this] + (as-uuid this)) + +(defprotocol VersionLike + (as-version [this])) + +(extend-protocol VersionLike + MAuthVersion + (as-version [this] this) + String + (as-version [this] (MAuthVersion/valueOf (str/upper-case this))) + Keyword + (as-version [this] (as-version (name this))) + Object + (as-version [this] (as-version (str this)))) + +(defn ->version + "Converts argument to an `MAuthVersion`. + To support additional types, extend the `VersionLike` protocol." + ^MAuthVersion [this] + (as-version this)) + +(defprotocol EpochTimeProviderLike + (as-epoch-time-provider ^EpochTimeProvider [this])) + +(extend-protocol EpochTimeProviderLike + EpochTimeProvider + (as-epoch-time-provider [this] this) + IFn + (as-epoch-time-provider [this] + (reify EpochTimeProvider + (inSeconds [_] + (long (this)))))) + +(defn ->epoch-time-provider + "Converts argument to an `EpochTimeProvider`. + To support additional types, extend the `EpochTimeProviderLike` protocol." + ^EpochTimeProvider [this] + (as-epoch-time-provider this)) + +;; The JVM does not have union types, so this is the best we can do +(defprotocol SerialData + (as-bytes-or-input-stream [this])) + +(extend-protocol SerialData + byte/1 + (as-bytes-or-input-stream [this] [:bytes this]) + Byte/1 + (as-bytes-or-input-stream [this] [:bytes (byte-array this)]) + String + (as-bytes-or-input-stream [this] [:bytes (.getBytes this "utf-8")]) + CharSequence + (as-bytes-or-input-stream [this] (as-bytes-or-input-stream (str this))) + ByteArrayInputStream + (as-bytes-or-input-stream [this] [:bytes (.readAllBytes this)]) + StringReader + (as-bytes-or-input-stream [this] (as-bytes-or-input-stream (slurp this))) + CharArrayReader + (as-bytes-or-input-stream [this] (as-bytes-or-input-stream (slurp this))) + nil + (as-bytes-or-input-stream [this] [:bytes this]) + InputStream + (as-bytes-or-input-stream [this] [:stream this])) + +(defn ->bytes-or-input-stream + "Converts argument to either `byte[]` or `InputStream`. + The return value is a vector whose first element is either `:bytes` or + `:stream`, and whose second element is a value of the corresponding type. + To support additional types, extend the `SerialData` protocol." + [this] + (as-bytes-or-input-stream this)) diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index a4ccf72..ba505f1 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -1,69 +1,15 @@ (ns com.mdsol.mauth.clojure.signer - (:require [clojure.string :as str]) - (:import (clojure.lang IFn Keyword) - (com.mdsol.mauth DefaultSigner - MAuthVersion - Signer) - (com.mdsol.mauth.util CurrentEpochTimeProvider EpochTimeProvider) - (java.io ByteArrayInputStream - CharArrayReader - InputStream - StringReader) - (java.util List UUID))) + (:require + [com.mdsol.mauth.clojure.convert :as convert]) + (:import + (com.mdsol.mauth DefaultSigner Signer) + (com.mdsol.mauth.util CurrentEpochTimeProvider EpochTimeProvider) + (java.io + InputStream) + (java.util List))) (set! *warn-on-reflection* true) -(defmulti ->uuid-impl - "Multimethod to convert an arbitrary type to `UUID`. - This multimethod underlies the `->uuid` function, which simply adds the - appropriate return type hint." - type) -(defmethod ->uuid-impl UUID [x] x) -(defmethod ->uuid-impl String [x] (parse-uuid x)) -(defmethod ->uuid-impl :default [x] (->uuid-impl (str x))) - -(defn ->uuid - "Converts an arbitrary type to `UUID`. - To extend support to additional types, define a new method for - `->uuid-impl`." - ^UUID [x] - (->uuid-impl x)) - -(defmulti ->version-impl - "Multimethod to convert an arbitrary type to `MAuthVersion`. - This multimethod underlies the `->version` function, which simply adds the - appropriate return type hint." - type) -(defmethod ->version-impl MAuthVersion [x] x) -(defmethod ->version-impl String [x] (MAuthVersion/valueOf (str/upper-case x))) -(defmethod ->version-impl :default [x] (->version-impl (str x))) -(defmethod ->version-impl Keyword [x] (->version-impl (name x))) - -(defn ->version - "Converts an arbitrary type to `MAuthVersion`. - To extend support to additional types, define a new method for - `->version-impl`." - ^MAuthVersion [x] - (->version-impl x)) - -(defmulti ->epoch-time-provider-impl - "Multimethod to convert an arbitrary type to `EpochTimeProvider`. - This multimethod underlies the `->epoch-time-provider` function, which simply - adds the appropriate return type hint." - type) -(defmethod ->epoch-time-provider-impl EpochTimeProvider [x] x) -(defmethod ->epoch-time-provider-impl IFn [x] - (reify EpochTimeProvider - (inSeconds [_this] - (long (x))))) - -(defn ->epoch-time-provider - "Converts an arbitrary type to `EpochTimeProvider`. - To extend support to additional types, define a new method for - `->epoch-time-provider-impl`." - ^EpochTimeProvider [x] - (->epoch-time-provider-impl x)) - (def current-epoch-time-provider "Provides the actual current time according to the system clock." (CurrentEpochTimeProvider.)) @@ -89,10 +35,10 @@ epoch-time-provider sign-versions] :or {epoch-time-provider current-epoch-time-provider sign-versions [:mwsv2]}}] - (DefaultSigner. (->uuid app-uuid) + (DefaultSigner. (convert/->uuid app-uuid) ^String private-key - (->epoch-time-provider epoch-time-provider) - ^List (list* (map ->version sign-versions)))) + ^EpochTimeProvider (convert/->epoch-time-provider epoch-time-provider) + ^List (list* (map convert/->version sign-versions)))) (comment (def my-signer @@ -127,35 +73,6 @@ JXNpe60qgURHJigvYmseF9p7f36w2cnGMpJowHhbY7QFYosuIOQ7Am8h24dgpHtd B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT -----END RSA PRIVATE KEY-----"}))) -;; The JVM does not have union types, so this is the best we can do -(defmulti ->array-or-input-stream - "Converts an arbitrary type to either `byte[]` or `InputStream`. - The return value is a vector whose first element is either `:array` or - `:input-stream`, and whose second element is a value of the corresponding - type." - type) - -;; Lazy reading -(defmethod ->array-or-input-stream InputStream [x] - [:input-stream x]) - -;; Eager reading -(defmethod ->array-or-input-stream (type (byte-array 0)) [x] - [:array x]) -(defmethod ->array-or-input-stream String [^String x] - (->array-or-input-stream (.getBytes x "UTF-8"))) -(defmethod ->array-or-input-stream CharSequence [x] - (->array-or-input-stream (str x))) -(defmethod ->array-or-input-stream ByteArrayInputStream - [^ByteArrayInputStream x] - (->array-or-input-stream (.readAllBytes x))) -(defmethod ->array-or-input-stream StringReader [^StringReader x] - (->array-or-input-stream (slurp x))) -(defmethod ->array-or-input-stream CharArrayReader [^CharArrayReader x] - (->array-or-input-stream (slurp x))) -(defmethod ->array-or-input-stream nil [_] - [:array nil]) - (defn gen-req-headers "Given a signer and a Ring request, returns a map of MAuth headers." [^Signer signer @@ -163,7 +80,7 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT (let [method (if (ident? request-method) (name request-method) ^String request-method) - [t b] (->array-or-input-stream body)] + [t b] (convert/->bytes-or-input-stream body)] (into {} ;; The InputStream overload can only provide signatures for one ;; version at a time, while the array overload requires holding the @@ -171,10 +88,10 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT ;; dispatch to one or the other depending on whether the request body ;; is already fully held in memory. (case t - :input-stream (.generateRequestHeaders signer method uri - ^InputStream b - query-string) - :array (.generateRequestHeaders signer method uri + :stream (.generateRequestHeaders signer method uri + ^InputStream b + query-string) + :bytes (.generateRequestHeaders signer method uri ^bytes b query-string))))) From a22f4790bf9dfa1510ec9d9a148884871c13cfea Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 11:43:39 -0400 Subject: [PATCH 17/45] Add authenticator --- project.clj | 6 +- src/com/mdsol/mauth/clojure/authenticator.clj | 77 +++++++++++++++++++ src/com/mdsol/mauth/clojure/convert.clj | 27 ++++++- src/com/mdsol/mauth/clojure/signer.clj | 1 + ...test.clj => signer_authenticator_test.clj} | 64 ++++++++++++--- 5 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/com/mdsol/mauth/clojure/authenticator.clj rename test/com/mdsol/mauth/clojure/{signer_test.clj => signer_authenticator_test.clj} (51%) diff --git a/project.clj b/project.clj index 1234b37..4ff50e2 100644 --- a/project.clj +++ b/project.clj @@ -14,9 +14,11 @@ [clj-http "3.13.0"] [org.clojure/data.json "2.5.0"] [javax.xml.bind/jaxb-api "2.3.1"] - [com.mdsol/mauth-signer "16.0.0"]] + [com.mdsol/mauth-authenticator "19.0.0"] + [com.mdsol/mauth-signer "19.0.0"]] - :profiles {:test {:dependencies [[com.mdsol/mauth-test-utils "16.0.0+0-a6fb9a5f+20240725-1833-SNAPSHOT"]]}} + :profiles {:test {:dependencies [[com.mdsol/mauth-authenticator-apachehttp "19.0.0"] + [com.mdsol/mauth-test-utils "19.0.0"]]}} :repositories [["maven-prod-virtual" {:url "https://mdsol.jfrog.io/mdsol/maven-prod-virtual" :username :env/artifactory_username diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj new file mode 100644 index 0000000..87ceb2f --- /dev/null +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -0,0 +1,77 @@ +(ns com.mdsol.mauth.clojure.authenticator + (:require + [com.mdsol.mauth.clojure.convert :as convert] + [com.mdsol.mauth.clojure.signer :as signer]) + (:import + (com.mdsol.mauth + Authenticator + MAuthRequest + MAuthRequest$Builder + RequestAuthenticator) + (com.mdsol.mauth.utils ClientPublicKeyProvider) + (java.net URI))) + +(comment + ;; construct AC + (def ac (AuthenticatorConfiguration. base-url + request-url-path + security-tokens-url-path)) + ;; construct Signer + (def signer (default-signer ...)) + ;; construct CPKP + ;; option 1: use apache one -- probably easier + (def key-provider (HttpClientPublicKeyProvider. ac signer)) + ;; option 2: reuse code from legacy client lib + ;; construct RA + (def authenticator (RequestAuthenticator. key-provider)) + ;; construct MAuthRequest + (def mauth-req + (MAuthRequest. authenticationHeaderValue + bodyInputStream ;; OR byte array + http-method + time-header-value + resource-path + query-parameters)) + + (.authenticate authenticator mauth-req)) + +;; TODO: Provide factory for using any HTTP client. For now, callers supply +;; their own ClientPublicKeyProvider impl. + +(defn- ->string ^String [x] + (if (ident? x) + (name x) + (str x))) + +(defn- mauth-request ^MAuthRequest [request] + (prn request) + (let [{:keys [request-method uri body headers query-string]} request + java-uri (URI. (str uri \? query-string)) + [t b] (convert/->bytes-or-input-stream body)] + (-> (MAuthRequest$Builder/get) + (.withHttpMethod (->string request-method)) + (.withResourcePath (.getRawPath java-uri)) + (.withQueryParameters (.getRawQuery java-uri)) + (.withMauthHeaders (-> headers + (update-keys ->string) + (update-vals ->string))) + (cond-> + ,(= :bytes t) (.withMessagePayload b) + (= :stream t) (.withBodyInputStream b)) + (.build)))) + +(defn default-authenticator ^RequestAuthenticator + [& {:keys [client-pk-provider + validation-timeout-seconds + epoch-time-provider + v2-only] + :or {validation-timeout-seconds 10 + epoch-time-provider signer/current-epoch-time-provider + v2-only false}}] + (RequestAuthenticator. ^ClientPublicKeyProvider client-pk-provider + (long validation-timeout-seconds) + (convert/->epoch-time-provider epoch-time-provider) + (boolean v2-only))) + +(defn valid? [^Authenticator authenticator request] + (.authenticate authenticator (mauth-request request))) diff --git a/src/com/mdsol/mauth/clojure/convert.clj b/src/com/mdsol/mauth/clojure/convert.clj index 46c2fc4..addc0cc 100644 --- a/src/com/mdsol/mauth/clojure/convert.clj +++ b/src/com/mdsol/mauth/clojure/convert.clj @@ -2,7 +2,7 @@ (:require [clojure.string :as str]) (:import - (clojure.lang IFn Keyword) + (clojure.lang IDeref IFn Keyword) (com.mdsol.mauth MAuthVersion) (com.mdsol.mauth.util EpochTimeProvider) (java.io @@ -10,7 +10,8 @@ CharArrayReader InputStream StringReader) - (java.util UUID))) + (java.util UUID) + [java.util.function IntSupplier LongSupplier Supplier])) (set! *warn-on-reflection* true) @@ -60,7 +61,27 @@ (as-epoch-time-provider [this] (reify EpochTimeProvider (inSeconds [_] - (long (this)))))) + (long (this))))) + IDeref + (as-epoch-time-provider [this] + (reify EpochTimeProvider + (inSeconds [_] + (long @this)))) + Supplier + (as-epoch-time-provider [this] + (reify EpochTimeProvider + (inSeconds [_] + (long (.get this))))) + LongSupplier + (as-epoch-time-provider [this] + (reify EpochTimeProvider + (inSeconds [_] + (.getAsLong this)))) + IntSupplier + (as-epoch-time-provider [this] + (reify EpochTimeProvider + (inSeconds [_] + (long (.getAsInt this)))))) (defn ->epoch-time-provider "Converts argument to an `EpochTimeProvider`. diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index ba505f1..93e8974 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -31,6 +31,7 @@ The types for all of these arguments are flexible. Support for new types can be added by installing new methods for the multimethods defined in this namespace." + ^DefaultSigner [& {:keys [app-uuid private-key epoch-time-provider sign-versions] :or {epoch-time-provider current-epoch-time-provider diff --git a/test/com/mdsol/mauth/clojure/signer_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj similarity index 51% rename from test/com/mdsol/mauth/clojure/signer_test.clj rename to test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index ece4149..405bf73 100644 --- a/test/com/mdsol/mauth/clojure/signer_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -1,12 +1,15 @@ -(ns com.mdsol.mauth.clojure.signer-test +(ns com.mdsol.mauth.clojure.signer-authenticator-test (:require [camel-snake-kebab.core :as csk] [charred.api :as charred] [clojure.java.io :as io] [clojure.string :as str] [clojure.test :refer [deftest is]] + [com.mdsol.mauth.clojure.authenticator :as auth] [com.mdsol.mauth.clojure.signer :as signer]) (:import + (com.mdsol.mauth.util MAuthKeysHelper) + (com.mdsol.mauth.utils ClientPublicKeyProvider) (java.io File FilenameFilter) (java.net URI))) @@ -32,9 +35,10 @@ (defn read-case [^File path] {:name (.getName path) - :request (some-> (child-by-ext path ".req") - (charred/read-json :key-fn csk/->kebab-case-keyword) - (ringify-request path)) + ;; Recreate request each time because some contain stateful streams + :request-fn #(some-> (child-by-ext path ".req") + (charred/read-json :key-fn csk/->kebab-case-keyword) + (ringify-request path)) ;; This implementation does not expose these intermediate steps in a testable ;; way. That's fine, because they aren't part of the public contract anyway ;; and don't really need to be tested directly. @@ -45,13 +49,16 @@ :headers (some-> (child-by-ext path ".authz") charred/read-json)}) +(def signing-config + (-> (io/file suite-base "signing-config.json") + (charred/read-json :key-fn csk/->kebab-case-keyword) + (update :app-uuid parse-uuid))) + (def signer - (let [{:keys [app-uuid request-time private-key-file]} - (charred/read-json (io/file suite-base "signing-config.json") - :key-fn csk/->kebab-case-keyword)] + (let [{:keys [app-uuid request-time private-key-file]} signing-config] (signer/default-signer :app-uuid app-uuid - :private-key (slurp (io/file suite-base private-key-file)) - :epoch-time-provider (constantly request-time)))) + :private-key (slurp (io/file suite-base private-key-file)) + :epoch-time-provider (constantly request-time)))) (def ignored-test-cases #{;; In HTTP, foo//bar is not the same as foo/bar. This case is incorrect. @@ -60,6 +67,7 @@ "get-vanilla-query-space"}) (def test-cases + ;; TODO: v1 cases (->> (io/file suite-base "protocols" "MWSV2") .listFiles (remove #(ignored-test-cases (.getName ^File %))) @@ -71,11 +79,43 @@ (update-vals str) vec)) +#_(use-fixtures :once + (fn [f] + (binding [*mauth-server-port* (PortFinder/findFreePort)] + (FakeMAuthServer/start *mauth-server-port*) + (try + (FakeMAuthServer/return200) + (Security/addProvider (BouncyCastleProvider.)) + (f) + (finally + (FakeMAuthServer/stop)))))) + +(def pub-key + (MAuthKeysHelper/getPublicKeyFromString + (slurp (io/file suite-base "signing-params" "rsa-key-pub") + :encoding "utf-8"))) + +(def pk-provider + (reify ClientPublicKeyProvider + (getPublicKey [_ app-uuid] + (if (= app-uuid (:app-uuid signing-config)) + pub-key + (throw (ex-info "Unexpected key requested" + {:received app-uuid + :expected (:app-uuid signing-config)})))))) + +(def authenticator + (auth/default-authenticator :client-pk-provider pk-provider + :epoch-time-provider (constantly 1444672125))) + ;; Index-based iteration, because some requests are input streams, and they ;; cannot be used as literals for evaluation. (doseq [i (range (count test-cases))] (eval `(deftest ~(-> test-cases (nth i) :name symbol) - (is (= ~(-> test-cases (nth i) :headers norm-headers) - (norm-headers (signer/gen-req-headers - signer (-> test-cases (nth ~i) :request)))))))) + (let [{:keys ~'[request-fn headers]} (nth test-cases ~i)] + (is (= (norm-headers ~'headers) + (norm-headers (signer/gen-req-headers signer (~'request-fn))))) + (is (true? (auth/valid? authenticator + (update (~'request-fn) :headers + merge ~'headers)))))))) From d136deef55e7a14ba1b0b3ebd9475652e9bb6584 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 11:47:48 -0400 Subject: [PATCH 18/45] Remove outdated comment --- test/com/mdsol/mauth/clojure/signer_authenticator_test.clj | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 405bf73..c209a54 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -108,8 +108,6 @@ (auth/default-authenticator :client-pk-provider pk-provider :epoch-time-provider (constantly 1444672125))) -;; Index-based iteration, because some requests are input streams, and they -;; cannot be used as literals for evaluation. (doseq [i (range (count test-cases))] (eval `(deftest ~(-> test-cases (nth i) :name symbol) From 57f001651af3123cc6978bc306fd26cbcca8db0b Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:28:09 -0400 Subject: [PATCH 19/45] Add v1 test cases --- .../clojure/signer_authenticator_test.clj | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index c209a54..71e2f22 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -37,8 +37,8 @@ {:name (.getName path) ;; Recreate request each time because some contain stateful streams :request-fn #(some-> (child-by-ext path ".req") - (charred/read-json :key-fn csk/->kebab-case-keyword) - (ringify-request path)) + (charred/read-json :key-fn csk/->kebab-case-keyword) + (ringify-request path)) ;; This implementation does not expose these intermediate steps in a testable ;; way. That's fine, because they aren't part of the public contract anyway ;; and don't really need to be tested directly. @@ -54,22 +54,32 @@ (charred/read-json :key-fn csk/->kebab-case-keyword) (update :app-uuid parse-uuid))) -(def signer +(def signer-v2 (let [{:keys [app-uuid request-time private-key-file]} signing-config] (signer/default-signer :app-uuid app-uuid :private-key (slurp (io/file suite-base private-key-file)) :epoch-time-provider (constantly request-time)))) +(def signer-v1 + (let [{:keys [app-uuid request-time private-key-file]} signing-config] + (signer/default-signer :app-uuid app-uuid + :private-key (slurp (io/file suite-base private-key-file)) + :epoch-time-provider (constantly request-time) + :sign-versions [:mws]))) + (def ignored-test-cases #{;; In HTTP, foo//bar is not the same as foo/bar. This case is incorrect. "get-normalize-multiple-slashes" ;; This is invalid URL syntax. Query strings may not contain spaces. "get-vanilla-query-space"}) -(def test-cases - ;; TODO: v1 cases - (->> (io/file suite-base "protocols" "MWSV2") - .listFiles +(def test-cases-v2 + (->> (.listFiles (io/file suite-base "protocols" "MWSV2")) + (remove #(ignored-test-cases (.getName ^File %))) + (map read-case))) + +(def test-cases-v1 + (->> (.listFiles (io/file suite-base "protocols" "MWS")) (remove #(ignored-test-cases (.getName ^File %))) (map read-case))) @@ -77,18 +87,8 @@ (-> m (update-keys str/lower-case) (update-vals str) - vec)) - -#_(use-fixtures :once - (fn [f] - (binding [*mauth-server-port* (PortFinder/findFreePort)] - (FakeMAuthServer/start *mauth-server-port*) - (try - (FakeMAuthServer/return200) - (Security/addProvider (BouncyCastleProvider.)) - (f) - (finally - (FakeMAuthServer/stop)))))) + vec + (->> (sort-by first)))) (def pub-key (MAuthKeysHelper/getPublicKeyFromString @@ -108,12 +108,30 @@ (auth/default-authenticator :client-pk-provider pk-provider :epoch-time-provider (constantly 1444672125))) -(doseq [i (range (count test-cases))] +(doseq [i (range (count test-cases-v2))] + (eval + `(deftest ~(-> test-cases-v2 + (nth i) + :name + (->> (str "mwsv2-")) + symbol) + (let [{:keys ~'[request-fn headers]} (nth test-cases-v2 ~i)] + (is (= (norm-headers ~'headers) + (norm-headers (signer/gen-req-headers signer-v2 (~'request-fn))))) + (is (true? (auth/valid? authenticator + (update (~'request-fn) :headers + merge ~'headers)))))))) + +(doseq [i (range (count test-cases-v1))] (eval - `(deftest ~(-> test-cases (nth i) :name symbol) - (let [{:keys ~'[request-fn headers]} (nth test-cases ~i)] + `(deftest ~(-> test-cases-v1 + (nth i) + :name + (->> (str "mws-")) + symbol) + (let [{:keys ~'[request-fn headers]} (nth test-cases-v1 ~i)] (is (= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer (~'request-fn))))) + (norm-headers (signer/gen-req-headers signer-v1 (~'request-fn))))) (is (true? (auth/valid? authenticator (update (~'request-fn) :headers merge ~'headers)))))))) From 7afae94ed07b83959104df8363f1e1454e34e1ba Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:36:12 -0400 Subject: [PATCH 20/45] Add humane-test-output --- project.clj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 4ff50e2..245bfde 100644 --- a/project.clj +++ b/project.clj @@ -18,7 +18,10 @@ [com.mdsol/mauth-signer "19.0.0"]] :profiles {:test {:dependencies [[com.mdsol/mauth-authenticator-apachehttp "19.0.0"] - [com.mdsol/mauth-test-utils "19.0.0"]]}} + [com.mdsol/mauth-test-utils "19.0.0"] + [pjstadig/humane-test-output "0.8.3"]] + :injections [(require 'pjstadig.humane-test-output) + (pjstadig.humane-test-output/activate!)]}} :repositories [["maven-prod-virtual" {:url "https://mdsol.jfrog.io/mdsol/maven-prod-virtual" :username :env/artifactory_username From 12c0e7c1909baf3b73a7880c6a7001c46039f38b Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:36:23 -0400 Subject: [PATCH 21/45] Add middleware test --- .../clojure/signer_authenticator_test.clj | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 71e2f22..383f979 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -116,11 +116,22 @@ (->> (str "mwsv2-")) symbol) (let [{:keys ~'[request-fn headers]} (nth test-cases-v2 ~i)] - (is (= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer-v2 (~'request-fn))))) + (is (~'= (norm-headers ~'headers) + (norm-headers (signer/gen-req-headers signer-v2 (~'request-fn)))) + "Signer produces expected headers") + ;; = only compares true on streams if it's the same instance + (let ~'[req (request-fn)] + (is (~'= (-> ~'req + (update :headers merge ~'headers) + (update :headers norm-headers)) + (update ((signer/wrap-client identity signer-v2) + ~'req) + :headers norm-headers)) + "Middleware adds expected headers")) (is (true? (auth/valid? authenticator (update (~'request-fn) :headers - merge ~'headers)))))))) + merge ~'headers))) + "Authenticator validates headers"))))) (doseq [i (range (count test-cases-v1))] (eval @@ -130,8 +141,19 @@ (->> (str "mws-")) symbol) (let [{:keys ~'[request-fn headers]} (nth test-cases-v1 ~i)] - (is (= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer-v1 (~'request-fn))))) + (is (~'= (norm-headers ~'headers) + (norm-headers (signer/gen-req-headers signer-v1 (~'request-fn)))) + "Signer produces expected headers") + ;; = only compares true on streams if it's the same instance + (let ~'[req (request-fn)] + (is (~'= (-> ~'req + (update :headers merge ~'headers) + (update :headers norm-headers)) + (update ((signer/wrap-client identity signer-v1) + ~'req) + :headers norm-headers)) + "Middleware adds expected headers")) (is (true? (auth/valid? authenticator (update (~'request-fn) :headers - merge ~'headers)))))))) + merge ~'headers))) + "Authenticator validates headers"))))) From c48ef47f2729e2e6164dd43096f664757ca28fdf Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:37:58 -0400 Subject: [PATCH 22/45] Remove RCF --- src/com/mdsol/mauth/clojure/authenticator.clj | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 87ceb2f..f7cb760 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -11,30 +11,6 @@ (com.mdsol.mauth.utils ClientPublicKeyProvider) (java.net URI))) -(comment - ;; construct AC - (def ac (AuthenticatorConfiguration. base-url - request-url-path - security-tokens-url-path)) - ;; construct Signer - (def signer (default-signer ...)) - ;; construct CPKP - ;; option 1: use apache one -- probably easier - (def key-provider (HttpClientPublicKeyProvider. ac signer)) - ;; option 2: reuse code from legacy client lib - ;; construct RA - (def authenticator (RequestAuthenticator. key-provider)) - ;; construct MAuthRequest - (def mauth-req - (MAuthRequest. authenticationHeaderValue - bodyInputStream ;; OR byte array - http-method - time-header-value - resource-path - query-parameters)) - - (.authenticate authenticator mauth-req)) - ;; TODO: Provide factory for using any HTTP client. For now, callers supply ;; their own ClientPublicKeyProvider impl. From b4c03220c14bcbc91dfc72ec031dbb2d4f3761b6 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:44:46 -0400 Subject: [PATCH 23/45] Add/revise docstrings --- src/com/mdsol/mauth/clojure/authenticator.clj | 22 +++++++++++++++++-- src/com/mdsol/mauth/clojure/signer.clj | 8 +++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index f7cb760..6347c60 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -36,7 +36,23 @@ (= :stream t) (.withBodyInputStream b)) (.build)))) -(defn default-authenticator ^RequestAuthenticator +(defn default-authenticator + "Returns an authenticator capable of validating MAuth signatures. + + Required arguments: + - client-pk-provider: An instance of + `com.mdsol.mauth.utils.ClientPublicKeyProvider` + Optional arguments: + - epoch-time-provider: A function which returns the current time as seconds + since the Unix epoch. Defaults to a function which returns the system clock + time. + - v2-only: If truthy, MAuth v1 requests will fail validation. Defaults to + `false`. + + The types for all of these arguments are flexible. Support for new types can + be added by extending the protocols defined in + `com.mdsol.mauth.clojure.convert`." + ^RequestAuthenticator [& {:keys [client-pk-provider validation-timeout-seconds epoch-time-provider @@ -49,5 +65,7 @@ (convert/->epoch-time-provider epoch-time-provider) (boolean v2-only))) -(defn valid? [^Authenticator authenticator request] +(defn valid? + "Returns `true` if the Ring request map passes `authenticator`'s validation." + [^Authenticator authenticator request] (.authenticate authenticator (mauth-request request))) diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index 93e8974..6b56c40 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -29,8 +29,8 @@ be produced. Defaults to `[:mwsv2]`. The types for all of these arguments are flexible. Support for new types can - be added by installing new methods for the multimethods defined in this - namespace." + be added by extending the protocols defined in + `com.mdsol.mauth.clojure.convert`." ^DefaultSigner [& {:keys [app-uuid private-key epoch-time-provider sign-versions] @@ -41,7 +41,7 @@ ^EpochTimeProvider (convert/->epoch-time-provider epoch-time-provider) ^List (list* (map convert/->version sign-versions)))) -(comment +(comment (def my-signer (default-signer {:sign-versions [:mws :mwsv2] :app-uuid (random-uuid) @@ -75,7 +75,7 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT -----END RSA PRIVATE KEY-----"}))) (defn gen-req-headers - "Given a signer and a Ring request, returns a map of MAuth headers." + "Given a signer and a Ring request map, returns a map of MAuth headers." [^Signer signer {:keys [request-method body ^String uri ^String query-string]}] (let [method (if (ident? request-method) From f44e936e5b0fd763ab6e38edb6d93bff607282bd Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:45:25 -0400 Subject: [PATCH 24/45] Add warn-on-reflection --- src/com/mdsol/mauth/clojure/authenticator.clj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 6347c60..e484268 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -11,6 +11,8 @@ (com.mdsol.mauth.utils ClientPublicKeyProvider) (java.net URI))) +(set! *warn-on-reflection* true) + ;; TODO: Provide factory for using any HTTP client. For now, callers supply ;; their own ClientPublicKeyProvider impl. From 4b6023d0f9a70cae0a870524e79bbfe8d9347cd4 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:32:34 -0400 Subject: [PATCH 25/45] Remove print statement --- src/com/mdsol/mauth/clojure/authenticator.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index e484268..52db788 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -22,7 +22,6 @@ (str x))) (defn- mauth-request ^MAuthRequest [request] - (prn request) (let [{:keys [request-method uri body headers query-string]} request java-uri (URI. (str uri \? query-string)) [t b] (convert/->bytes-or-input-stream body)] From 8aa39d49e538981e8710b59086e6a1a2ad215b38 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:36:05 -0400 Subject: [PATCH 26/45] Add server middleware with simple test case --- src/com/mdsol/mauth/clojure/authenticator.clj | 25 +++++++++++++++++++ .../clojure/signer_authenticator_test.clj | 15 ++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 52db788..59aa482 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -70,3 +70,28 @@ "Returns `true` if the Ring request map passes `authenticator`'s validation." [^Authenticator authenticator request] (.authenticate authenticator (mauth-request request))) + +(def ^:private default-401 + {:status 401 + :body "MAuth authentication failed."}) + +(defn default-on-auth-failure + ([_request] + default-401) + ([_request respond _raise] + (respond default-401))) + +(defn wrap-handler + ([handler authenticator] + (wrap-handler handler authenticator {})) + ([handler authenticator {:keys [on-auth-failure] + :or {on-auth-failure default-on-auth-failure}}] + (fn + ([request] + (if (valid? authenticator request) + (handler request) + (on-auth-failure request))) + ([request respond raise] + (if (valid? authenticator request) + (handler request respond raise) + (on-auth-failure request respond raise)))))) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 383f979..eac06fa 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -117,7 +117,7 @@ symbol) (let [{:keys ~'[request-fn headers]} (nth test-cases-v2 ~i)] (is (~'= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer-v2 (~'request-fn)))) + (norm-headers (signer/gen-req-headers signer-v2 (~'request-fn)))) "Signer produces expected headers") ;; = only compares true on streams if it's the same instance (let ~'[req (request-fn)] @@ -142,7 +142,7 @@ symbol) (let [{:keys ~'[request-fn headers]} (nth test-cases-v1 ~i)] (is (~'= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer-v1 (~'request-fn)))) + (norm-headers (signer/gen-req-headers signer-v1 (~'request-fn)))) "Signer produces expected headers") ;; = only compares true on streams if it's the same instance (let ~'[req (request-fn)] @@ -152,8 +152,15 @@ (update ((signer/wrap-client identity signer-v1) ~'req) :headers norm-headers)) - "Middleware adds expected headers")) + "Client middleware adds expected headers")) (is (true? (auth/valid? authenticator (update (~'request-fn) :headers merge ~'headers))) - "Authenticator validates headers"))))) + "Authenticator validates headers") + (let [~'req (update (~'request-fn) :headers + merge ~'headers)] + (is (~'= ~'req #_{:status 401 :body "oops!"} + ((auth/wrap-handler identity authenticator) + (update ~'req :headers + merge ~'headers))) + "Server middleware passes through on success")))))) From 0137344287bc7cac2cc449e9238566b84215a967 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:39:08 -0400 Subject: [PATCH 27/45] Deduplicate test code --- .../clojure/signer_authenticator_test.clj | 69 ++++++++----------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index eac06fa..aaef792 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -108,6 +108,32 @@ (auth/default-authenticator :client-pk-provider pk-provider :epoch-time-provider (constantly 1444672125))) +(defn validate-test-case [test-case signer] + (let [{:keys [request-fn headers]} test-case] + (is (= (norm-headers headers) + (norm-headers (signer/gen-req-headers signer (request-fn)))) + "Signer produces expected headers") + ;; = only compares true on streams if it's the same instance + (let [req (request-fn)] + (is (= (-> req + (update :headers merge headers) + (update :headers norm-headers)) + (update ((signer/wrap-client identity signer) + req) + :headers norm-headers)) + "Client middleware adds expected headers")) + (is (true? (auth/valid? authenticator + (update (request-fn) :headers + merge headers))) + "Authenticator validates headers") + (let [req (update (request-fn) :headers + merge headers)] + (is (= req #_{:status 401 :body "oops!"} + ((auth/wrap-handler identity authenticator) + (update req :headers + merge headers))) + "Server middleware passes through on success")))) + (doseq [i (range (count test-cases-v2))] (eval `(deftest ~(-> test-cases-v2 @@ -115,23 +141,7 @@ :name (->> (str "mwsv2-")) symbol) - (let [{:keys ~'[request-fn headers]} (nth test-cases-v2 ~i)] - (is (~'= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer-v2 (~'request-fn)))) - "Signer produces expected headers") - ;; = only compares true on streams if it's the same instance - (let ~'[req (request-fn)] - (is (~'= (-> ~'req - (update :headers merge ~'headers) - (update :headers norm-headers)) - (update ((signer/wrap-client identity signer-v2) - ~'req) - :headers norm-headers)) - "Middleware adds expected headers")) - (is (true? (auth/valid? authenticator - (update (~'request-fn) :headers - merge ~'headers))) - "Authenticator validates headers"))))) + (validate-test-case (nth test-cases-v2 ~i) signer-v2)))) (doseq [i (range (count test-cases-v1))] (eval @@ -140,27 +150,4 @@ :name (->> (str "mws-")) symbol) - (let [{:keys ~'[request-fn headers]} (nth test-cases-v1 ~i)] - (is (~'= (norm-headers ~'headers) - (norm-headers (signer/gen-req-headers signer-v1 (~'request-fn)))) - "Signer produces expected headers") - ;; = only compares true on streams if it's the same instance - (let ~'[req (request-fn)] - (is (~'= (-> ~'req - (update :headers merge ~'headers) - (update :headers norm-headers)) - (update ((signer/wrap-client identity signer-v1) - ~'req) - :headers norm-headers)) - "Client middleware adds expected headers")) - (is (true? (auth/valid? authenticator - (update (~'request-fn) :headers - merge ~'headers))) - "Authenticator validates headers") - (let [~'req (update (~'request-fn) :headers - merge ~'headers)] - (is (~'= ~'req #_{:status 401 :body "oops!"} - ((auth/wrap-handler identity authenticator) - (update ~'req :headers - merge ~'headers))) - "Server middleware passes through on success")))))) + (validate-test-case (nth test-cases-v1 ~i) signer-v1)))) From 87a9e508f29a29b592e262f679c45214be22bcba Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:26:44 -0400 Subject: [PATCH 28/45] Add negative test case for server middleware --- src/com/mdsol/mauth/clojure/authenticator.clj | 29 ++++++++++++------- src/com/mdsol/mauth/clojure/signer.clj | 9 +++--- .../clojure/signer_authenticator_test.clj | 23 +++++++++++++-- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 59aa482..2a6ab88 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -8,6 +8,7 @@ MAuthRequest MAuthRequest$Builder RequestAuthenticator) + (com.mdsol.mauth.exception MAuthValidationException) (com.mdsol.mauth.utils ClientPublicKeyProvider) (java.net URI))) @@ -69,17 +70,19 @@ (defn valid? "Returns `true` if the Ring request map passes `authenticator`'s validation." [^Authenticator authenticator request] - (.authenticate authenticator (mauth-request request))) + (try + (.authenticate authenticator (mauth-request request)))) (def ^:private default-401 {:status 401 :body "MAuth authentication failed."}) (defn default-on-auth-failure - ([_request] + ([_request _exception] default-401) - ([_request respond _raise] - (respond default-401))) + ;; TODO: Support async + #_([_request respond _raise] + (respond default-401))) (defn wrap-handler ([handler authenticator] @@ -88,10 +91,14 @@ :or {on-auth-failure default-on-auth-failure}}] (fn ([request] - (if (valid? authenticator request) - (handler request) - (on-auth-failure request))) - ([request respond raise] - (if (valid? authenticator request) - (handler request respond raise) - (on-auth-failure request respond raise)))))) + (try + (if (valid? authenticator request) + (handler request) + (on-auth-failure request nil)) + (catch MAuthValidationException e + (on-auth-failure request e)))) + ;; TODO: Support async + #_([request respond raise] + (if (valid? authenticator request) + (handler request respond raise) + (on-auth-failure request respond raise)))))) diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index 6b56c40..92e99e3 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -41,7 +41,7 @@ ^EpochTimeProvider (convert/->epoch-time-provider epoch-time-provider) ^List (list* (map convert/->version sign-versions)))) -(comment +(comment (def my-signer (default-signer {:sign-versions [:mws :mwsv2] :app-uuid (random-uuid) @@ -108,9 +108,10 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT (fn ([req] (client (update req :headers merge (gen-req-headers signer req)))) - ([req respond raise] - (client (update req :headers merge (gen-req-headers signer req)) - respond raise)))) + ;; TODO: Support async + #_([req respond raise] + (client (update req :headers merge (gen-req-headers signer req)) + respond raise)))) (comment ((wrap-client prn my-signer) {:request-method :post diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index aaef792..8795fa7 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -128,11 +128,30 @@ "Authenticator validates headers") (let [req (update (request-fn) :headers merge headers)] - (is (= req #_{:status 401 :body "oops!"} + (is (= req ((auth/wrap-handler identity authenticator) (update req :headers merge headers))) - "Server middleware passes through on success")))) + "Server middleware passes through on success")) + (is (= {:status 401 :body "oops!"} + ((auth/wrap-handler identity authenticator + {:on-auth-failure (constantly + {:status 401 + :body "oops!"})}) + (-> (request-fn) + (update :headers merge headers) + (assoc :body "This is not the right body!")))) + "Server middleware calls on-auth-failure on failure") + (is (= {:status 401 :body "oops!"} + ((auth/wrap-handler identity authenticator + {:on-auth-failure (constantly + {:status 401 + :body "oops!"})}) + (-> (request-fn) + (update :headers merge headers) + (assoc-in [:headers "X-MWS-Time"] 1) + (assoc-in [:headers "MCC-Time"] 1)))) + "Server middleware calls on-auth-failure on exception"))) (doseq [i (range (count test-cases-v2))] (eval From 471c68a4cd1cdacbecfe3bd6374c1a227b96a6ed Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:29:05 -0400 Subject: [PATCH 29/45] Prove that on-auth-failure receives request --- .../mauth/clojure/signer_authenticator_test.clj | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 8795fa7..b8a4058 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -133,14 +133,18 @@ (update req :headers merge headers))) "Server middleware passes through on success")) - (is (= {:status 401 :body "oops!"} + (is (= {:status 401 + :body "oops!" + ::got-request true} ((auth/wrap-handler identity authenticator - {:on-auth-failure (constantly - {:status 401 - :body "oops!"})}) + {:on-auth-failure (fn [{::keys [is-request]} _exc] + {:status 401 + :body "oops!" + ::got-request is-request})}) (-> (request-fn) (update :headers merge headers) - (assoc :body "This is not the right body!")))) + (assoc :body "This is not the right body!") + (assoc ::is-request true)))) "Server middleware calls on-auth-failure on failure") (is (= {:status 401 :body "oops!"} ((auth/wrap-handler identity authenticator From 340ac5c6f0d8aadb946a60faf88f60e81658909b Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:29:53 -0400 Subject: [PATCH 30/45] Assert nil exception on valid=false --- test/com/mdsol/mauth/clojure/signer_authenticator_test.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index b8a4058..112360d 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -137,7 +137,8 @@ :body "oops!" ::got-request true} ((auth/wrap-handler identity authenticator - {:on-auth-failure (fn [{::keys [is-request]} _exc] + {:on-auth-failure (fn [{::keys [is-request]} exc] + (is (nil? exc)) {:status 401 :body "oops!" ::got-request is-request})}) From 0a93f26b142819b986799ad539e1f640684d6d99 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:33:40 -0400 Subject: [PATCH 31/45] Prove that on-auth-failure receives exception --- .../clojure/signer_authenticator_test.clj | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 112360d..b940c76 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -8,6 +8,7 @@ [com.mdsol.mauth.clojure.authenticator :as auth] [com.mdsol.mauth.clojure.signer :as signer]) (:import + (com.mdsol.mauth.exception MAuthValidationException) (com.mdsol.mauth.util MAuthKeysHelper) (com.mdsol.mauth.utils ClientPublicKeyProvider) (java.io File FilenameFilter) @@ -137,25 +138,32 @@ :body "oops!" ::got-request true} ((auth/wrap-handler identity authenticator - {:on-auth-failure (fn [{::keys [is-request]} exc] - (is (nil? exc)) - {:status 401 - :body "oops!" - ::got-request is-request})}) + {:on-auth-failure + (fn [{::keys [is-request]} exc] + (is (nil? exc)) + {:status 401 + :body "oops!" + ::got-request is-request})}) (-> (request-fn) (update :headers merge headers) (assoc :body "This is not the right body!") (assoc ::is-request true)))) "Server middleware calls on-auth-failure on failure") - (is (= {:status 401 :body "oops!"} + (is (= {:status 401 + :body "oops!" + ::got-request true} ((auth/wrap-handler identity authenticator - {:on-auth-failure (constantly - {:status 401 - :body "oops!"})}) + {:on-auth-failure + (fn [{::keys [is-request]} exc] + (is (instance? MAuthValidationException exc)) + {:status 401 + :body "oops!" + ::got-request is-request})}) (-> (request-fn) (update :headers merge headers) (assoc-in [:headers "X-MWS-Time"] 1) - (assoc-in [:headers "MCC-Time"] 1)))) + (assoc-in [:headers "MCC-Time"] 1) + (assoc ::is-request true)))) "Server middleware calls on-auth-failure on exception"))) (doseq [i (range (count test-cases-v2))] From 13c32188e2481786c1c20d33909049472f861c72 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:44:09 -0400 Subject: [PATCH 32/45] Change on-auth-failure to accept a map This way we can add more values in the future without a breaking change. --- src/com/mdsol/mauth/clojure/authenticator.clj | 18 +++++++++++++++--- .../clojure/signer_authenticator_test.clj | 15 +++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 2a6ab88..4c56302 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -78,13 +78,22 @@ :body "MAuth authentication failed."}) (defn default-on-auth-failure - ([_request _exception] + ([_] default-401) ;; TODO: Support async #_([_request respond _raise] (respond default-401))) (defn wrap-handler + "Middleware for handlers conforming to the Ring signature. + + Options: + - on-auth-failure: A function called when authentication fails, which should + return a response. Defaults to `default-on-auth-failure`. It receives a map + with the following keys: + - request: the request which failed authentication + - handler: the handler wrapped by this middleware + - exception: (optional) the exception thrown during validation, if any" ([handler authenticator] (wrap-handler handler authenticator {})) ([handler authenticator {:keys [on-auth-failure] @@ -94,9 +103,12 @@ (try (if (valid? authenticator request) (handler request) - (on-auth-failure request nil)) + (on-auth-failure {:request request + :handler handler})) (catch MAuthValidationException e - (on-auth-failure request e)))) + (on-auth-failure {:request request + :handler handler + :exception e})))) ;; TODO: Support async #_([request respond raise] (if (valid? authenticator request) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index b940c76..80d9aae 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -139,11 +139,12 @@ ::got-request true} ((auth/wrap-handler identity authenticator {:on-auth-failure - (fn [{::keys [is-request]} exc] - (is (nil? exc)) + (fn [{:keys [request exception handler]}] + (is (nil? exception)) + (is (= identity handler)) {:status 401 :body "oops!" - ::got-request is-request})}) + ::got-request (::is-request request)})}) (-> (request-fn) (update :headers merge headers) (assoc :body "This is not the right body!") @@ -154,11 +155,13 @@ ::got-request true} ((auth/wrap-handler identity authenticator {:on-auth-failure - (fn [{::keys [is-request]} exc] - (is (instance? MAuthValidationException exc)) + (fn [{:keys [request exception handler]}] + (is (instance? MAuthValidationException + exception)) + (is (= identity handler)) {:status 401 :body "oops!" - ::got-request is-request})}) + ::got-request (::is-request request)})}) (-> (request-fn) (update :headers merge headers) (assoc-in [:headers "X-MWS-Time"] 1) From 0f3df05f1011002649b84525577592c82c3fbd99 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:45:33 -0400 Subject: [PATCH 33/45] Add javadoc --- src/com/mdsol/mauth/clojure/authenticator.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 4c56302..0c5bfa7 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -78,6 +78,7 @@ :body "MAuth authentication failed."}) (defn default-on-auth-failure + "Returns a static map with a 401 response." ([_] default-401) ;; TODO: Support async From a65a47aa06ac61173bd26a9b06fffac056d581a4 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:48:53 -0400 Subject: [PATCH 34/45] Use exception message for response body --- src/com/mdsol/mauth/clojure/authenticator.clj | 11 +++++------ .../mdsol/mauth/clojure/signer_authenticator_test.clj | 11 ++++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 0c5bfa7..da6ed5e 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -73,14 +73,13 @@ (try (.authenticate authenticator (mauth-request request)))) -(def ^:private default-401 - {:status 401 - :body "MAuth authentication failed."}) - (defn default-on-auth-failure "Returns a static map with a 401 response." - ([_] - default-401) + ([{:keys [exception]}] + {:status 401 + :body {:message (if exception + (ex-message exception) + "MAuth authentication failed.")}}) ;; TODO: Support async #_([_request respond _raise] (respond default-401))) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 80d9aae..ad612cf 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -167,7 +167,16 @@ (assoc-in [:headers "X-MWS-Time"] 1) (assoc-in [:headers "MCC-Time"] 1) (assoc ::is-request true)))) - "Server middleware calls on-auth-failure on exception"))) + "Server middleware calls on-auth-failure on exception") + (is (= {:status 401 + :body {:message "MAuth request validation failed because request time was older than10s"}} + ((auth/wrap-handler identity authenticator) + (-> (request-fn) + (update :headers merge headers) + (assoc-in [:headers "X-MWS-Time"] 1) + (assoc-in [:headers "MCC-Time"] 1) + (assoc ::is-request true)))) + "default-on-auth-failure returns exception message on exception"))) (doseq [i (range (count test-cases-v2))] (eval From 2b57309d6984faffdd25b189c2dc375f2e55388a Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:50:16 -0400 Subject: [PATCH 35/45] Add test for default message on auth failure --- .../mauth/clojure/signer_authenticator_test.clj | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index ad612cf..68f85b8 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -174,9 +174,15 @@ (-> (request-fn) (update :headers merge headers) (assoc-in [:headers "X-MWS-Time"] 1) - (assoc-in [:headers "MCC-Time"] 1) - (assoc ::is-request true)))) - "default-on-auth-failure returns exception message on exception"))) + (assoc-in [:headers "MCC-Time"] 1)))) + "default-on-auth-failure returns exception message on exception") + (is (= {:status 401 + :body {:message "MAuth authentication failed."}} + ((auth/wrap-handler identity authenticator) + (-> (request-fn) + (update :headers merge headers) + (assoc :body "This is not the right body!")))) + "default-on-auth-failure returns default message on auth failure"))) (doseq [i (range (count test-cases-v2))] (eval From a971e2a95bb683f1ac1d8b5d7f8968314dd99984 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 16:13:36 -0400 Subject: [PATCH 36/45] Remove unused dependency --- project.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/project.clj b/project.clj index 245bfde..5788d66 100644 --- a/project.clj +++ b/project.clj @@ -18,7 +18,6 @@ [com.mdsol/mauth-signer "19.0.0"]] :profiles {:test {:dependencies [[com.mdsol/mauth-authenticator-apachehttp "19.0.0"] - [com.mdsol/mauth-test-utils "19.0.0"] [pjstadig/humane-test-output "0.8.3"]] :injections [(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)]}} From ace2029a25d7995edcbb7c78ca0f3f1c30d93c18 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 16:32:51 -0400 Subject: [PATCH 37/45] Fix MD rendering of docstring --- src/com/mdsol/mauth/clojure/signer.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/mdsol/mauth/clojure/signer.clj b/src/com/mdsol/mauth/clojure/signer.clj index 92e99e3..d306453 100644 --- a/src/com/mdsol/mauth/clojure/signer.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -21,6 +21,7 @@ - app-uuid: The UUID registered in the MAuth server for the signing application. - private-key: The application's private key as a String. + Optional arguments: - epoch-time-provider: A function which returns the current time as seconds since the Unix epoch. Defaults to a function which returns the system clock From d4329ec31738a07b0af2899f91aad4c759ee4a9d Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 16:55:04 -0400 Subject: [PATCH 38/45] Fix MD rendering of docstring --- src/com/mdsol/mauth/clojure/authenticator.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index da6ed5e..c37287a 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -44,6 +44,7 @@ Required arguments: - client-pk-provider: An instance of `com.mdsol.mauth.utils.ClientPublicKeyProvider` + Optional arguments: - epoch-time-provider: A function which returns the current time as seconds since the Unix epoch. Defaults to a function which returns the system clock From fa5241475bbb643218972e41790348322e0bbd93 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 09:43:50 -0400 Subject: [PATCH 39/45] Refactor to expose MAuth data --- src/com/mdsol/mauth/clojure/authenticator.clj | 43 +++++++++++++------ .../clojure/signer_authenticator_test.clj | 5 +-- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index c37287a..54b63ea 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -1,5 +1,6 @@ (ns com.mdsol.mauth.clojure.authenticator (:require + [clojure.string :as str] [com.mdsol.mauth.clojure.convert :as convert] [com.mdsol.mauth.clojure.signer :as signer]) (:import @@ -22,7 +23,9 @@ (name x) (str x))) -(defn- mauth-request ^MAuthRequest [request] +(defn- mauth-request + "Constructs an `MAuthRequest` object from a Ring request map." + ^MAuthRequest [request] (let [{:keys [request-method uri body headers query-string]} request java-uri (URI. (str uri \? query-string)) [t b] (convert/->bytes-or-input-stream body)] @@ -68,11 +71,24 @@ (convert/->epoch-time-provider epoch-time-provider) (boolean v2-only))) +(defn mauth-data + "Extracts key MAuth information from a Ring request and constructs a `MAuthRequest`." + [request] + (let [mauth-req (mauth-request request)] + {:app-uuid (.getAppUUID mauth-req) + :mauth-version (-> mauth-req + .getMauthVersion + .name + str/lower-case + keyword) + :mauth-request-object mauth-req})) + (defn valid? - "Returns `true` if the Ring request map passes `authenticator`'s validation." + "Returns `true` if the request passes `authenticator`'s validation." [^Authenticator authenticator request] - (try - (.authenticate authenticator (mauth-request request)))) + (let [mauth-req (or (:mauth-request-object request) + (:mauth-request-object (mauth-data request)))] + (.authenticate authenticator mauth-req))) (defn default-on-auth-failure "Returns a static map with a 401 response." @@ -101,15 +117,16 @@ :or {on-auth-failure default-on-auth-failure}}] (fn ([request] - (try - (if (valid? authenticator request) - (handler request) - (on-auth-failure {:request request - :handler handler})) - (catch MAuthValidationException e - (on-auth-failure {:request request - :handler handler - :exception e})))) + (let [data (mauth-data request)] + (try + (if (valid? authenticator data) + (handler (assoc request :com.mdsol.mauth/app-uuid (:app-uuid data))) + (on-auth-failure {:request request + :handler handler})) + (catch MAuthValidationException e + (on-auth-failure {:request request + :handler handler + :exception e}))))) ;; TODO: Support async #_([request respond raise] (if (valid? authenticator request) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 68f85b8..862b457 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -129,7 +129,7 @@ "Authenticator validates headers") (let [req (update (request-fn) :headers merge headers)] - (is (= req + (is (= (assoc req :com.mdsol.mauth/app-uuid (:app-uuid signing-config)) ((auth/wrap-handler identity authenticator) (update req :headers merge headers))) @@ -156,7 +156,7 @@ ((auth/wrap-handler identity authenticator {:on-auth-failure (fn [{:keys [request exception handler]}] - (is (instance? MAuthValidationException + (is (instance? MAuthValidationException exception)) (is (= identity handler)) {:status 401 @@ -192,7 +192,6 @@ (->> (str "mwsv2-")) symbol) (validate-test-case (nth test-cases-v2 ~i) signer-v2)))) - (doseq [i (range (count test-cases-v1))] (eval `(deftest ~(-> test-cases-v1 From 61b01cd82d413357bfa5ab0c748317f9264b06d3 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:19:38 -0400 Subject: [PATCH 40/45] Remove submodule config --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ff92868..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "mauth-protocol-test-suite"] - path = mauth-protocol-test-suite - url = git@github.com:mdsol/mauth-protocol-test-suite.git From a3a731461182025481cb5262d8ac745e35f5bcb5 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:19:48 -0400 Subject: [PATCH 41/45] Vendor mauth test suite This works around an issue in our CI build where it cannot check out the submodule. Personally, I think vendoring is a better practice than using submodules anyway - way simpler, way harder to mess up. --- mauth-protocol-test-suite/.version | 1 + mauth-protocol-test-suite/CHANGELOG.md | 12 ++ mauth-protocol-test-suite/LICENSE | 202 ++++++++++++++++++ mauth-protocol-test-suite/NOTICE | 4 + mauth-protocol-test-suite/README.md | 28 +++ .../MWS/binary-body/binary-body.authz | 4 + .../protocols/MWS/binary-body/binary-body.req | 5 + .../protocols/MWS/binary-body/binary-body.sig | 1 + .../protocols/MWS/binary-body/readme.md | 2 + .../protocols/MWS/binary-body/white_1x1.png | Bin 0 -> 73 bytes .../MWS/get-unreserved/get-unreserved.authz | 4 + .../MWS/get-unreserved/get-unreserved.req | 5 + .../MWS/get-unreserved/get-unreserved.sig | 1 + .../MWS/get-unreserved/get-unreserved.sts | 5 + .../MWS/get-vanilla/get-vanilla.authz | 4 + .../protocols/MWS/get-vanilla/get-vanilla.req | 5 + .../protocols/MWS/get-vanilla/get-vanilla.sig | 1 + .../protocols/MWS/get-vanilla/get-vanilla.sts | 5 + .../post-vanilla-body-empty.authz | 4 + .../post-vanilla-body-empty.req | 5 + .../post-vanilla-body-empty.sig | 1 + .../post-vanilla-body-empty.sts | 5 + .../post-vanilla-body/post-vanilla-body.authz | 4 + .../post-vanilla-body/post-vanilla-body.req | 5 + .../post-vanilla-body/post-vanilla-body.sig | 1 + .../post-vanilla-body/post-vanilla-body.sts | 5 + .../authentication-only-after-decoding.authz | 4 + .../authentication-only-after-decoding.req | 5 + .../readme.md | 1 + .../MWSV2/binary-body/binary-body.authz | 4 + .../MWSV2/binary-body/binary-body.req | 5 + .../MWSV2/binary-body/binary-body.sig | 1 + .../MWSV2/binary-body/binary-body.sts | 5 + .../protocols/MWSV2/binary-body/readme.md | 1 + .../protocols/MWSV2/binary-body/white_1x1.png | Bin 0 -> 73 bytes .../get-normalize-multiple-slashes.authz | 4 + .../get-normalize-multiple-slashes.req | 5 + .../get-normalize-multiple-slashes.sig | 1 + .../get-normalize-multiple-slashes.sts | 5 + .../get-normalize-parent-in-path.authz | 4 + .../get-normalize-parent-in-path.req | 5 + .../get-normalize-parent-in-path.sig | 1 + .../get-normalize-parent-in-path.sts | 5 + .../get-normalize-self-in-path.authz | 4 + .../get-normalize-self-in-path.req | 5 + .../get-normalize-self-in-path.sig | 1 + .../get-normalize-self-in-path.sts | 5 + .../MWSV2/get-unreserved/get-unreserved.authz | 4 + .../MWSV2/get-unreserved/get-unreserved.req | 5 + .../MWSV2/get-unreserved/get-unreserved.sig | 1 + .../MWSV2/get-unreserved/get-unreserved.sts | 5 + .../get-utf8-normalize-case.authz | 4 + .../get-utf8-normalize-case.req | 5 + .../get-utf8-normalize-case.sig | 1 + .../get-utf8-normalize-case.sts | 5 + .../get-vanilla-query-encoded-tilde.authz | 4 + .../get-vanilla-query-encoded-tilde.req | 5 + .../get-vanilla-query-encoded-tilde.sig | 1 + .../get-vanilla-query-encoded-tilde.sts | 6 + .../get-vanilla-query-no-value.authz | 4 + .../get-vanilla-query-no-value.req | 5 + .../get-vanilla-query-no-value.sig | 1 + .../get-vanilla-query-no-value.sts | 6 + ...get-vanilla-query-param-key-ordering.authz | 4 + .../get-vanilla-query-param-key-ordering.req | 5 + .../get-vanilla-query-param-key-ordering.sig | 1 + .../get-vanilla-query-param-key-ordering.sts | 6 + ...t-vanilla-query-param-value-ordering.authz | 4 + ...get-vanilla-query-param-value-ordering.req | 5 + ...get-vanilla-query-param-value-ordering.sig | 1 + ...get-vanilla-query-param-value-ordering.sts | 6 + .../get-vanilla-query-space.authz | 4 + .../get-vanilla-query-space.req | 5 + .../get-vanilla-query-space.sig | 1 + .../get-vanilla-query-space.sts | 6 + .../get-vanilla-query-unreserved.authz | 4 + .../get-vanilla-query-unreserved.req | 5 + .../get-vanilla-query-unreserved.sig | 1 + .../get-vanilla-query-unreserved.sts | 6 + ...et-vanilla-query-utf8-normalize-case.authz | 4 + .../get-vanilla-query-utf8-normalize-case.req | 5 + .../get-vanilla-query-utf8-normalize-case.sig | 1 + .../get-vanilla-query-utf8-normalize-case.sts | 6 + .../get-vanilla-query-utf8.authz | 4 + .../get-vanilla-query-utf8.req | 5 + .../get-vanilla-query-utf8.sig | 1 + .../get-vanilla-query-utf8.sts | 6 + .../MWSV2/get-vanilla/get-vanilla.authz | 4 + .../MWSV2/get-vanilla/get-vanilla.req | 5 + .../MWSV2/get-vanilla/get-vanilla.sig | 1 + .../MWSV2/get-vanilla/get-vanilla.sts | 5 + .../post-vanilla-body-empty.authz | 4 + .../post-vanilla-body-empty.req | 5 + .../post-vanilla-body-empty.sig | 1 + .../post-vanilla-body-empty.sts | 5 + .../post-vanilla-body-query.authz | 4 + .../post-vanilla-body-query.req | 5 + .../post-vanilla-body-query.sig | 1 + .../post-vanilla-body-query.sts | 6 + .../post-vanilla-body/post-vanilla-body.authz | 4 + .../post-vanilla-body/post-vanilla-body.req | 5 + .../post-vanilla-body/post-vanilla-body.sig | 1 + .../post-vanilla-body/post-vanilla-body.sts | 5 + mauth-protocol-test-suite/signing-config.json | 5 + .../signing-params/rsa-key | 27 +++ .../signing-params/rsa-key-pub | 9 + 106 files changed, 650 insertions(+) create mode 100644 mauth-protocol-test-suite/.version create mode 100644 mauth-protocol-test-suite/CHANGELOG.md create mode 100644 mauth-protocol-test-suite/LICENSE create mode 100644 mauth-protocol-test-suite/NOTICE create mode 100644 mauth-protocol-test-suite/README.md create mode 100644 mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.authz create mode 100644 mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.req create mode 100644 mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.sig create mode 100644 mauth-protocol-test-suite/protocols/MWS/binary-body/readme.md create mode 100644 mauth-protocol-test-suite/protocols/MWS/binary-body/white_1x1.png create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-unreserved/get-unreserved.authz create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-unreserved/get-unreserved.req create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-unreserved/get-unreserved.sig create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-unreserved/get-unreserved.sts create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-vanilla/get-vanilla.authz create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-vanilla/get-vanilla.req create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-vanilla/get-vanilla.sig create mode 100644 mauth-protocol-test-suite/protocols/MWS/get-vanilla/get-vanilla.sts create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body-empty/post-vanilla-body-empty.authz create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body-empty/post-vanilla-body-empty.req create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body-empty/post-vanilla-body-empty.sig create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body-empty/post-vanilla-body-empty.sts create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body/post-vanilla-body.authz create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body/post-vanilla-body.req create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body/post-vanilla-body.sig create mode 100644 mauth-protocol-test-suite/protocols/MWS/post-vanilla-body/post-vanilla-body.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/authentication-only-after-decoding/authentication-only-after-decoding.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/authentication-only-after-decoding/authentication-only-after-decoding.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/authentication-only-after-decoding/readme.md create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/binary-body/binary-body.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/binary-body/binary-body.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/binary-body/binary-body.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/binary-body/binary-body.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/binary-body/readme.md create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/binary-body/white_1x1.png create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-multiple-slashes/get-normalize-multiple-slashes.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-multiple-slashes/get-normalize-multiple-slashes.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-multiple-slashes/get-normalize-multiple-slashes.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-multiple-slashes/get-normalize-multiple-slashes.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-parent-in-path/get-normalize-parent-in-path.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-parent-in-path/get-normalize-parent-in-path.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-parent-in-path/get-normalize-parent-in-path.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-parent-in-path/get-normalize-parent-in-path.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-self-in-path/get-normalize-self-in-path.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-self-in-path/get-normalize-self-in-path.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-self-in-path/get-normalize-self-in-path.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-normalize-self-in-path/get-normalize-self-in-path.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-unreserved/get-unreserved.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-unreserved/get-unreserved.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-unreserved/get-unreserved.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-unreserved/get-unreserved.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-utf8-normalize-case/get-utf8-normalize-case.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-utf8-normalize-case/get-utf8-normalize-case.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-utf8-normalize-case/get-utf8-normalize-case.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-utf8-normalize-case/get-utf8-normalize-case.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-encoded-tilde/get-vanilla-query-encoded-tilde.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-encoded-tilde/get-vanilla-query-encoded-tilde.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-encoded-tilde/get-vanilla-query-encoded-tilde.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-encoded-tilde/get-vanilla-query-encoded-tilde.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-no-value/get-vanilla-query-no-value.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-no-value/get-vanilla-query-no-value.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-no-value/get-vanilla-query-no-value.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-no-value/get-vanilla-query-no-value.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-key-ordering/get-vanilla-query-param-key-ordering.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-key-ordering/get-vanilla-query-param-key-ordering.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-key-ordering/get-vanilla-query-param-key-ordering.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-key-ordering/get-vanilla-query-param-key-ordering.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-value-ordering/get-vanilla-query-param-value-ordering.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-value-ordering/get-vanilla-query-param-value-ordering.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-value-ordering/get-vanilla-query-param-value-ordering.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-param-value-ordering/get-vanilla-query-param-value-ordering.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-space/get-vanilla-query-space.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-space/get-vanilla-query-space.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-space/get-vanilla-query-space.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-space/get-vanilla-query-space.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-unreserved/get-vanilla-query-unreserved.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-unreserved/get-vanilla-query-unreserved.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-unreserved/get-vanilla-query-unreserved.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-unreserved/get-vanilla-query-unreserved.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8-normalize-case/get-vanilla-query-utf8-normalize-case.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8-normalize-case/get-vanilla-query-utf8-normalize-case.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8-normalize-case/get-vanilla-query-utf8-normalize-case.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8-normalize-case/get-vanilla-query-utf8-normalize-case.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8/get-vanilla-query-utf8.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8/get-vanilla-query-utf8.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8/get-vanilla-query-utf8.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla-query-utf8/get-vanilla-query-utf8.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla/get-vanilla.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla/get-vanilla.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla/get-vanilla.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/get-vanilla/get-vanilla.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-empty/post-vanilla-body-empty.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-empty/post-vanilla-body-empty.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-empty/post-vanilla-body-empty.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-empty/post-vanilla-body-empty.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-query/post-vanilla-body-query.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-query/post-vanilla-body-query.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-query/post-vanilla-body-query.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body-query/post-vanilla-body-query.sts create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body/post-vanilla-body.authz create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body/post-vanilla-body.req create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body/post-vanilla-body.sig create mode 100644 mauth-protocol-test-suite/protocols/MWSV2/post-vanilla-body/post-vanilla-body.sts create mode 100644 mauth-protocol-test-suite/signing-config.json create mode 100644 mauth-protocol-test-suite/signing-params/rsa-key create mode 100644 mauth-protocol-test-suite/signing-params/rsa-key-pub diff --git a/mauth-protocol-test-suite/.version b/mauth-protocol-test-suite/.version new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/mauth-protocol-test-suite/.version @@ -0,0 +1 @@ +0.2.0 diff --git a/mauth-protocol-test-suite/CHANGELOG.md b/mauth-protocol-test-suite/CHANGELOG.md new file mode 100644 index 0000000..ac26d0e --- /dev/null +++ b/mauth-protocol-test-suite/CHANGELOG.md @@ -0,0 +1,12 @@ +# 0.2.0 +* Add MWS(V1) test suite. + +# 0.1.2 +* Use valid UTF-8 codes in `get-vanilla-query-param-key-ordering`. + +# 0.1.1 +* Fix query param ordering in case `post-vanilla-body-query`. +* Use UTF-8 URL encoding instead of Windows-1252 encoding in `get-vanilla-query-param-value-ordering`. + +# 0.1.0 +* Created MWSV2 test suite. diff --git a/mauth-protocol-test-suite/LICENSE b/mauth-protocol-test-suite/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/mauth-protocol-test-suite/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/mauth-protocol-test-suite/NOTICE b/mauth-protocol-test-suite/NOTICE new file mode 100644 index 0000000..b64bbce --- /dev/null +++ b/mauth-protocol-test-suite/NOTICE @@ -0,0 +1,4 @@ +Medidata mAuth Protocol 4 Test Suite + +This product includes software developed at +Amazon.com, Inc. (https://www.amazon.com/). diff --git a/mauth-protocol-test-suite/README.md b/mauth-protocol-test-suite/README.md new file mode 100644 index 0000000..4dbb748 --- /dev/null +++ b/mauth-protocol-test-suite/README.md @@ -0,0 +1,28 @@ +# mauth-protocol-test-suite + +This repo contains test cases for mAuth digital signature protocols and was created to aid in the development of mAuth clients in a language agnostic way. Currently the repo contains cases for the MWS and MWSV2 protocols. The MWSV2 protocol specification is described [here](https://learn.mdsol.com/display/CA/MAuth+Protocol+V2+Specification). The repo also serves as an description of mAuth protocol specifications with examples. + +## Usage + +This repo should be added to each mAuth client via git submodules (see documentation [here](https://git-scm.com/book/en/v2/Git-Tools-Submodules) and run as part of the test suite for that mAuth client to ensure it conforms to the mAuth protocol specification. mAuth clients are expected to write some glue code that will allow them to run the cases provided here with their testing tool (Rspec etc). + +### Test Cases + +For each there are four files with the following extensions: `.req`, `.sts`, `.sig`, `.authz`. + +The `.req` files contain a JSON hash of the attributes of an unsigned request. +The `.sts` files contain the `string_to_sign` (the string that will be passed through mAuth client's hashing algorithm) for that request. +The `.sig` files contain the digital signature of that request. +The `.authz` files contain a JSON hash of the authentication headers that would be added to that request in order to sign it. + +For each case, clients that sign requests should run three tests: +1. Given the request attributes in the `.req` file, the client should generate a string_to_sign that matches the `.sts` file. +2. Given the string_to_sign in the `.sts` file, the client should generate a digital signature that matches the `.sig` file. +3. Given the signature in the `.sig` file, the client should generate authentication headers that match the headers in the `.authz` file. + +Clients that authenticate requests should also run an additional test: +1. Combining the authentication headers in the `.authz` file and the request attributes in the `.req` file into a signed request, the client should consider the request authentic. + +### Signing Parameters + +The mAuth client running these tests should sign requests with the [provided RSA private key](./signing-params/rsa-key) and authenticate requests with the [provided RSA public key](./signing-params/rsa-key-pub). All requests should be signed and authenticated with the `app_uuid` and `request_time` provided in [signing-config.json](./signing-config.json). If the testing mAuth client does not accept the request time as an argument some library that mocks time APIs (i.e. Timecop for Ruby) should be used. diff --git a/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.authz b/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.authz new file mode 100644 index 0000000..da32c92 --- /dev/null +++ b/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.authz @@ -0,0 +1,4 @@ +{ + "X-MWS-Authentication": "MWS 836a454e-7f14-4192-8f5a-2a9d3d66f70c:HIrmHSjmK7Q91Xwm+zYvudeIV+RcOMJoEc7bUfeM6+1ReQ7tsd1ankm1BFSTy3a/iCMPx4k5F4NKxxCdCunYV5QdM51hQiJfHB6ALg1WfSnfNnZtOWZAC78R2X3709G4mozJCB0GJ3ERm5GMUiL4ncuGz4wWuw3RGJHDwcFH7F55NnCgdL2nBJ8e0MLGGaDd8np8Kk0HQOyAT867t+g0/SHR7ZN/cfQILtdFKmMRC0Jpx8Sv4NQwsfYq/LldPTBb2EPKyzcbk7JKc5CKPcSZ3GMlYS8DpTAL94PQKfRQd8pcI/bFqUX/uwcqiCFlLFSj15UBOsiVYsif2crTrc5lKQ==", + "X-MWS-Time": 1585082925 +} diff --git a/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.req b/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.req new file mode 100644 index 0000000..4d1d10f --- /dev/null +++ b/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.req @@ -0,0 +1,5 @@ +{ + "verb": "POST", + "url": "/", + "body_filepath": "./white_1x1.png" +} diff --git a/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.sig b/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.sig new file mode 100644 index 0000000..ce68bd4 --- /dev/null +++ b/mauth-protocol-test-suite/protocols/MWS/binary-body/binary-body.sig @@ -0,0 +1 @@ +HIrmHSjmK7Q91Xwm+zYvudeIV+RcOMJoEc7bUfeM6+1ReQ7tsd1ankm1BFSTy3a/iCMPx4k5F4NKxxCdCunYV5QdM51hQiJfHB6ALg1WfSnfNnZtOWZAC78R2X3709G4mozJCB0GJ3ERm5GMUiL4ncuGz4wWuw3RGJHDwcFH7F55NnCgdL2nBJ8e0MLGGaDd8np8Kk0HQOyAT867t+g0/SHR7ZN/cfQILtdFKmMRC0Jpx8Sv4NQwsfYq/LldPTBb2EPKyzcbk7JKc5CKPcSZ3GMlYS8DpTAL94PQKfRQd8pcI/bFqUX/uwcqiCFlLFSj15UBOsiVYsif2crTrc5lKQ== \ No newline at end of file diff --git a/mauth-protocol-test-suite/protocols/MWS/binary-body/readme.md b/mauth-protocol-test-suite/protocols/MWS/binary-body/readme.md new file mode 100644 index 0000000..b802682 --- /dev/null +++ b/mauth-protocol-test-suite/protocols/MWS/binary-body/readme.md @@ -0,0 +1,2 @@ +This case for binary request bodies is slightly different than the other cases. mAuth clients running this test should read in the [blank.jpeg](./blank.jpeg) file as the request body. +The `.sts` file is intentionally omitted because the MWS protocol uses the binary request body itself as a part of the `string_to_sign`. diff --git a/mauth-protocol-test-suite/protocols/MWS/binary-body/white_1x1.png b/mauth-protocol-test-suite/protocols/MWS/binary-body/white_1x1.png new file mode 100644 index 0000000000000000000000000000000000000000..3d95bc8ba34646d2227e9442fec14a51ae5f67a0 GIT binary patch literal 73 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}bl0Z$jlkcwN3tPH>YGyDev&fm;` U_geq11YGyDev&fm;` U_geq11 Date: Fri, 16 May 2025 10:25:19 -0400 Subject: [PATCH 42/45] Add alpha1 qualifier to version --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 5788d66..fe285d4 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject clojure-mauth-client "3.0.0-SNAPSHOT" +(defproject clojure-mauth-client "3.0.0-alpha1-SNAPSHOT" :description "Clojure Mauth Client" :url "https://github.com/mdsol/clojure-mauth-client" :license {:name "MIT" From 3fb4f918a3a1eaa3d45e8826c2e15d23ac574b7d Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:30:00 -0400 Subject: [PATCH 43/45] Reduce mauth dep version --- project.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project.clj b/project.clj index fe285d4..e7a48ae 100644 --- a/project.clj +++ b/project.clj @@ -14,10 +14,10 @@ [clj-http "3.13.0"] [org.clojure/data.json "2.5.0"] [javax.xml.bind/jaxb-api "2.3.1"] - [com.mdsol/mauth-authenticator "19.0.0"] - [com.mdsol/mauth-signer "19.0.0"]] + [com.mdsol/mauth-authenticator "17.0.0"] + [com.mdsol/mauth-signer "17.0.0"]] - :profiles {:test {:dependencies [[com.mdsol/mauth-authenticator-apachehttp "19.0.0"] + :profiles {:test {:dependencies [[com.mdsol/mauth-authenticator-apachehttp "17.0.0"] [pjstadig/humane-test-output "0.8.3"]] :injections [(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)]}} From 27ebe4addcf4055f1aaf4803cff11de3961e2f14 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 10:40:36 -0400 Subject: [PATCH 44/45] Match error message in test to v17 --- test/com/mdsol/mauth/clojure/signer_authenticator_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index 862b457..c2bd9af 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -169,7 +169,7 @@ (assoc ::is-request true)))) "Server middleware calls on-auth-failure on exception") (is (= {:status 401 - :body {:message "MAuth request validation failed because request time was older than10s"}} + :body {:message "MAuth request validation failed because of timeout 10s"}} ((auth/wrap-handler identity authenticator) (-> (request-fn) (update :headers merge headers) From 0314b0b6674264eea839cc1d6d0b5514ecb402c5 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Fri, 16 May 2025 11:18:05 -0400 Subject: [PATCH 45/45] Report additional errors as auth fail --- src/com/mdsol/mauth/clojure/authenticator.clj | 31 +++++++++++++------ .../clojure/signer_authenticator_test.clj | 26 ++++++++++++---- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj index 54b63ea..b9bcc1a 100644 --- a/src/com/mdsol/mauth/clojure/authenticator.clj +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -4,6 +4,7 @@ [com.mdsol.mauth.clojure.convert :as convert] [com.mdsol.mauth.clojure.signer :as signer]) (:import + (clojure.lang ExceptionInfo) (com.mdsol.mauth Authenticator MAuthRequest @@ -92,11 +93,9 @@ (defn default-on-auth-failure "Returns a static map with a 401 response." - ([{:keys [exception]}] + ([_] {:status 401 - :body {:message (if exception - (ex-message exception) - "MAuth authentication failed.")}}) + :body {:message "Unauthorized."}}) ;; TODO: Support async #_([_request respond _raise] (respond default-401))) @@ -117,16 +116,28 @@ :or {on-auth-failure default-on-auth-failure}}] (fn ([request] - (let [data (mauth-data request)] - (try - (if (valid? authenticator data) + (try + (let [data (try + (mauth-data request) + (catch IllegalArgumentException e + (throw (ex-info "Invalid authentication headers." + {:type ::auth-fail} + e))))] + (if (try + (valid? authenticator data) + (catch MAuthValidationException e + (throw (ex-info "Failed authentication." + {:type ::auth-fail} + e)))) (handler (assoc request :com.mdsol.mauth/app-uuid (:app-uuid data))) (on-auth-failure {:request request - :handler handler})) - (catch MAuthValidationException e + :handler handler}))) + (catch ExceptionInfo e + (if (= ::auth-fail (:type (ex-data e))) (on-auth-failure {:request request :handler handler - :exception e}))))) + :exception e}) + (throw e))))) ;; TODO: Support async #_([request respond raise] (if (valid? authenticator request) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj index c2bd9af..d2789e7 100644 --- a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -149,15 +149,29 @@ (update :headers merge headers) (assoc :body "This is not the right body!") (assoc ::is-request true)))) - "Server middleware calls on-auth-failure on failure") + "Server middleware calls on-auth-failure on mismatched signature") (is (= {:status 401 :body "oops!" ::got-request true} ((auth/wrap-handler identity authenticator {:on-auth-failure (fn [{:keys [request exception handler]}] - (is (instance? MAuthValidationException - exception)) + (is (instance? Exception exception)) + (is (= identity handler)) + {:status 401 + :body "oops!" + ::got-request (::is-request request)})}) + (-> (request-fn) + (assoc :headers {}) + (assoc ::is-request true)))) + "Server middleware calls on-auth-failure on missing auth headers.") + (is (= {:status 401 + :body "oops!" + ::got-request true} + ((auth/wrap-handler identity authenticator + {:on-auth-failure + (fn [{:keys [request exception handler]}] + (is (instance? Exception exception)) (is (= identity handler)) {:status 401 :body "oops!" @@ -167,9 +181,9 @@ (assoc-in [:headers "X-MWS-Time"] 1) (assoc-in [:headers "MCC-Time"] 1) (assoc ::is-request true)))) - "Server middleware calls on-auth-failure on exception") + "Server middleware calls on-auth-failure on expired signature") (is (= {:status 401 - :body {:message "MAuth request validation failed because of timeout 10s"}} + :body {:message "Unauthorized."}} ((auth/wrap-handler identity authenticator) (-> (request-fn) (update :headers merge headers) @@ -177,7 +191,7 @@ (assoc-in [:headers "MCC-Time"] 1)))) "default-on-auth-failure returns exception message on exception") (is (= {:status 401 - :body {:message "MAuth authentication failed."}} + :body {:message "Unauthorized."}} ((auth/wrap-handler identity authenticator) (-> (request-fn) (update :headers merge headers)