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/32] 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 ab94235e1b4f3462673dba7e7aaafd4c16992f00 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:07:37 -0400 Subject: [PATCH 02/32] WIP --- .clj-kondo/config.edn | 28 ++++ .gitmodules | 3 + mauth-protocol-test-suite | 1 + project.clj | 38 +++-- .../mauth/clojure/{client.clj => signer.clj} | 148 ++++++++++++------ test/com/mdsol/mauth/clojure/signer_test.clj | 78 +++++++++ 6 files changed, 230 insertions(+), 66 deletions(-) create mode 100644 .clj-kondo/config.edn create mode 100644 .gitmodules create mode 160000 mauth-protocol-test-suite rename src/com/mdsol/mauth/clojure/{client.clj => signer.clj} (52%) create mode 100644 test/com/mdsol/mauth/clojure/signer_test.clj 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}}} 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 diff --git a/mauth-protocol-test-suite b/mauth-protocol-test-suite new file mode 160000 index 0000000..bab0b0d --- /dev/null +++ b/mauth-protocol-test-suite @@ -0,0 +1 @@ +Subproject commit bab0b0dbfdf39e340f4a4179f62e64cd87cf4f96 diff --git a/project.clj b/project.clj index 5c85f81..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"] @@ -14,6 +17,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 @@ -27,23 +34,20 @@ ["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"] - [])) - :aot :all) + [] ;other opts... + (if (let [v (-> (System/getProperty "java.version") + (clojure.string/split #"[.]") + first + Integer.)] + (and (>= v 9) (< v 11))) + ["--add-modules" "java.xml.bind"] + []))) diff --git a/src/com/mdsol/mauth/clojure/client.clj b/src/com/mdsol/mauth/clojure/signer.clj similarity index 52% rename from src/com/mdsol/mauth/clojure/client.clj rename to src/com/mdsol/mauth/clojure/signer.clj index e101a21..a4ccf72 100644 --- a/src/com/mdsol/mauth/clojure/client.clj +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -1,62 +1,102 @@ -(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) + (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) -(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] +(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 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] +(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 type) -(defmethod ->epoch-time-provider-impl EpochTimeProvider [x] - 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 ^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) ^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----- @@ -88,7 +128,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] @@ -108,10 +153,13 @@ 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 [^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) @@ -131,12 +179,14 @@ B8+UoQ/ICy2ahrEljIQOLSqekDRq8QaRSpIZ2MNFVRPtH85R/zmxrVvT query-string))))) (comment - (gen-req-headers signer {:request-method :post - :uri "/foo" - :body "Hey hey"}) - ) - -(defn wrap-client [signer client] + (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. + Adds MAuth headers to requests." + [client signer] (fn ([req] (client (update req :headers merge (gen-req-headers signer req)))) @@ -145,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"})) 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..343b70f --- /dev/null +++ b/test/com/mdsol/mauth/clojure/signer_test.clj @@ -0,0 +1,78 @@ +(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 9a9b4d748e163d73699be33901e1073b09ebdf6e Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:12:17 -0400 Subject: [PATCH 03/32] Remove legacy implementation --- project.clj | 14 +- src/clojure_mauth_client/credentials.clj | 23 -- src/clojure_mauth_client/header.clj | 43 ---- src/clojure_mauth_client/header_v2.clj | 34 --- src/clojure_mauth_client/middleware.clj | 43 ---- src/clojure_mauth_client/request.clj | 70 ------ src/clojure_mauth_client/util.clj | 37 --- src/clojure_mauth_client/validate.clj | 45 ---- test/clojure_mauth_client/header_test.clj | 55 ----- test/clojure_mauth_client/header_v2_test.clj | 54 ----- test/clojure_mauth_client/middleware_test.clj | 82 ------- test/clojure_mauth_client/request_test.clj | 218 ------------------ test/clojure_mauth_client/validate_test.clj | 63 ----- 13 files changed, 6 insertions(+), 775 deletions(-) delete mode 100644 src/clojure_mauth_client/credentials.clj delete mode 100644 src/clojure_mauth_client/header.clj delete mode 100644 src/clojure_mauth_client/header_v2.clj delete mode 100644 src/clojure_mauth_client/middleware.clj delete mode 100644 src/clojure_mauth_client/request.clj delete mode 100644 src/clojure_mauth_client/util.clj delete mode 100644 src/clojure_mauth_client/validate.clj delete mode 100644 test/clojure_mauth_client/header_test.clj delete mode 100644 test/clojure_mauth_client/header_v2_test.clj delete mode 100644 test/clojure_mauth_client/middleware_test.clj delete mode 100644 test/clojure_mauth_client/request_test.clj delete mode 100644 test/clojure_mauth_client/validate_test.clj diff --git a/project.clj b/project.clj index 8d24f5e..d1d8b1a 100644 --- a/project.clj +++ b/project.clj @@ -7,14 +7,12 @@ [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"] - [clojure-interop/java.security "1.0.5"] - [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"] + #_[xsc/pem-reader "0.1.1"] + #_[digest "1.4.10"] + #_[org.clojure/data.codec "0.1.1"] + #_[clojure-interop/java.security "1.0.5"] + #_[org.clojure/data.json "2.5.0"] + #_[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" diff --git a/src/clojure_mauth_client/credentials.clj b/src/clojure_mauth_client/credentials.clj deleted file mode 100644 index 0f32f46..0000000 --- a/src/clojure_mauth_client/credentials.clj +++ /dev/null @@ -1,23 +0,0 @@ -(ns clojure-mauth-client.credentials - (:require [clojure.string :refer [blank?]])) - -(defonce ^:private credential (atom {})) - -(defn define-credentials [app-uuid private-key mauth-service-url] - (let [cred {:app-uuid app-uuid - :private-key private-key - :mauth-service-url mauth-service-url}] - (if (or (blank? app-uuid) - (blank? private-key) - (blank? mauth-service-url)) - (throw (Exception. "Please be sure the app-uuid, private-key and mauth-service-url are defined and valid, - either in the environment variables APP_UUID, MAUTH_KEY and MAUTH_SERVICE_URL respectively. - Or, you may call define-credentials to set them manually.")) - (reset! credential cred)))) - -(defn get-credentials [] - (if (empty? @credential) - (define-credentials (System/getenv "APP_UUID") - (System/getenv "MAUTH_KEY") - (System/getenv "MAUTH_SERVICE_URL"))) - @credential) \ No newline at end of file diff --git a/src/clojure_mauth_client/header.clj b/src/clojure_mauth_client/header.clj deleted file mode 100644 index 5144b8e..0000000 --- a/src/clojure_mauth_client/header.clj +++ /dev/null @@ -1,43 +0,0 @@ -(ns clojure-mauth-client.header - (:require - [clojure-mauth-client.util :as util] - [clojure.data.codec.base64 :as base64] - [clojure.string :as str] - [pem-reader.core :as pem]) - (:use digest) - (:import - (javax.crypto Cipher))) - -(defn- sign-mauth [message app-uuid private-key] - (->> (let [private-key (pem/private-key (util/read-key private-key)) - cipher (doto (Cipher/getInstance "RSA/ECB/PKCS1Padding") - (.init Cipher/ENCRYPT_MODE private-key))] - (.doFinal cipher (.getBytes message))) - base64/encode - String. - (str "MWS " app-uuid ":"))) - -(defn- make-mws-auth-string [verb url body app-uuid time] - (->> [verb (util/get-uri url) body app-uuid time] - (str/join "\n"))) - -(defn- make-mws-auth-string-for-response [status body app-uuid time] - (->> [status body app-uuid time] - (str/join "\n"))) - -(defn build-mauth-headers - ([verb url body app-uuid private-key] - (let [x-mws-time (util/epoch-seconds) - x-mws-authentication (make-mws-auth-string verb url body app-uuid x-mws-time)] - {"X-MWS-Authentication" (-> x-mws-authentication - util/msg->sha512 - (sign-mauth app-uuid private-key)) - "X-MWS-Time" (str x-mws-time)})) - ([status body app-uuid private-key] - (let [x-mws-time (util/epoch-seconds) - x-mws-authentication (make-mws-auth-string-for-response status body app-uuid x-mws-time)] - {"X-MWS-Authentication" (-> x-mws-authentication - util/msg->sha512 - (sign-mauth app-uuid private-key)) - "X-MWS-Time" (str x-mws-time)}))) - diff --git a/src/clojure_mauth_client/header_v2.clj b/src/clojure_mauth_client/header_v2.clj deleted file mode 100644 index 613e7a0..0000000 --- a/src/clojure_mauth_client/header_v2.clj +++ /dev/null @@ -1,34 +0,0 @@ -(ns clojure-mauth-client.header-v2 - (:require [pem-reader.core :as pem] - [clojure.data.codec.base64 :as base64] - [jdk.security.Signature :as signature] - [clojure-mauth-client.util :as util])) - -(defn- encrypt-signature-rsa [private-key-string string-to-sign] - (let [signature-instance (signature/*get-instance "SHA512withRSA") - private-key (pem/private-key (util/read-key private-key-string)) - byte-array-to-sign (util/str->bytes (String. string-to-sign))] - (signature/init-sign signature-instance private-key) - (signature/update signature-instance byte-array-to-sign 0 (alength byte-array-to-sign)) - (->> (signature/sign signature-instance) - base64/encode - String. - (str "")))) - -(defn- generate-headers-v2 [mcc-auth-string-to-sign app-uuid private-key] - (let [encrypted-signature (encrypt-signature-rsa private-key mcc-auth-string-to-sign) - mcc-authentication (str "MWSV2" " " app-uuid ":" encrypted-signature ";")] - mcc-authentication)) - -(defn build-mauth-headers-v2 - ([verb-or-status body app-uuid private-key] - (build-mauth-headers-v2 verb-or-status nil body app-uuid private-key nil)) - ([verb-or-status url body app-uuid private-key query-param] - (let [mcc-time (util/epoch-seconds) - params-vec (if (instance? java.lang.String verb-or-status) - [verb-or-status (util/get-uri url) (util/get-hex-encoded-digested-string body) app-uuid mcc-time query-param] - [verb-or-status (util/get-hex-encoded-digested-string body) app-uuid mcc-time]) - all-params-string (clojure.string/join "\n" params-vec) - authentication (generate-headers-v2 all-params-string app-uuid private-key)] - {"mcc-authentication" authentication - "mcc-time" (str mcc-time)}))) \ No newline at end of file diff --git a/src/clojure_mauth_client/middleware.clj b/src/clojure_mauth_client/middleware.clj deleted file mode 100644 index 30b9776..0000000 --- a/src/clojure_mauth_client/middleware.clj +++ /dev/null @@ -1,43 +0,0 @@ -(ns clojure-mauth-client.middleware - (:require [clojure.string :refer [lower-case]] - [clojure.data.json :as json]) - (:use clojure-mauth-client.validate)) - -(defn- downcase-header-keys [headers] - (reduce-kv - (fn [m k v] - (assoc m (lower-case (name k)) v)) - {} headers)) - -(defn wrap-mauth-verification [handler] - (fn [request] - (let [{method :request-method - uri :uri - body :body - query-string :query-string} request - serialized-body (cond (nil? body) "" - (string? body) body - (map? body) (json/write-str body) - :else (slurp body)) - headers (-> (:headers request) - downcase-header-keys) - [mauth-time mauth-auth] (cond - (every? headers - ["mcc-time" - "mcc-authentication"]) [(get headers "mcc-time") - (get headers "mcc-authentication")] - (every? headers - ["x-mws-time" - "x-mws-authentication"]) [(get headers "x-mws-time") - (get headers "x-mws-authentication")]) - mauth-version (let [signature (signature-map mauth-auth) - token (:token signature)] - (cond - (= token "MWSV2") "v2" - (= token "MWS") "v1")) - valid? (validate! (.toUpperCase (name method)) uri serialized-body mauth-time mauth-auth mauth-version query-string)] - (if valid? - (handler (-> request - (assoc :body serialized-body))) - {:status 401 - :body "Unauthorized."})))) diff --git a/src/clojure_mauth_client/request.clj b/src/clojure_mauth_client/request.clj deleted file mode 100644 index 8bd8c8e..0000000 --- a/src/clojure_mauth_client/request.clj +++ /dev/null @@ -1,70 +0,0 @@ -(ns clojure-mauth-client.request - (:require [org.httpkit.client :as http] - [clj-http.client :as client] - [clojure.data.json :as json] - [clojure-mauth-client.header :as header] - [clojure-mauth-client.header-v2 :as header-v2] - [clojure-mauth-client.credentials :as credentials]) - (:import (javax.net.ssl SSLEngine SNIHostName SSLParameters) - (java.net URI))) - -(defn- sni-configure - [^SSLEngine ssl-engine ^URI uri] - (let [^SSLParameters ssl-params (.getSSLParameters ssl-engine)] - (.setServerNames ssl-params [(SNIHostName. (.getHost uri))]) - (.setSSLParameters ssl-engine ssl-params))) - -(defn build-header [mauth-version query-string params] - (let [params-with-query-string (conj params query-string)] - (if (or (not (seq mauth-version)) (not (^String .equalsIgnoreCase mauth-version "v2"))) - (apply header/build-mauth-headers params) - (apply header-v2/build-mauth-headers-v2 params-with-query-string)))) - -(defn make-request [type base-url uri body & {:keys [additional-headers with-sni? throw-exceptions?] - :or {additional-headers {} - with-sni? nil - throw-exceptions? false}}] - (let [cred (credentials/get-credentials) - mauth-version (additional-headers :mauth-version) - query-params (additional-headers :query-param-string) - ; Tech debt: test with-sni?=true and modify this code if needed - ; (https://jira.mdsol.com/browse/MCC-767309) - options (if with-sni? {:client (http/make-client {:ssl-configurer sni-configure})} {}) - response (-> [(.toUpperCase type) (str base-url uri) (str body) (:app-uuid cred) (:private-key cred)] - (#(build-header mauth-version query-params %)) - (merge additional-headers) - ; We use clj-http instead of http-kit because it is supported by the motel-java tracing agent - (#(client/request (-> {:headers % - :url (str base-url uri) - :method (keyword (.toLowerCase type)) - :body body - :throw-exceptions throw-exceptions? - :as :auto} - (merge options)))))] - ; The following line is here because existing clients expect a String instead of a LazySeq. - ; When we're ready to make a breaking change, we should return "response" directly with no modification. - (update response :body json/write-str))) - -(defn get! [base-url uri & {:keys [additional-headers with-sni?] - :or {additional-headers {} - with-sni? nil}}] - (make-request "GET" base-url uri "" :additional-headers additional-headers - :with-sni? with-sni?)) - -(defn post! [base-url uri body & {:keys [additional-headers with-sni?] - :or {additional-headers {} - with-sni? nil}}] - (make-request "POST" base-url uri body :additional-headers additional-headers - :with-sni? with-sni?)) - -(defn delete! [base-url uri & {:keys [additional-headers with-sni?] - :or {additional-headers {} - with-sni? nil}}] - (make-request "DELETE" base-url uri "" :additional-headers additional-headers - :with-sni? with-sni?)) - -(defn put! [base-url uri body & {:keys [additional-headers with-sni?] - :or {additional-headers {} - with-sni? nil}}] - (make-request "PUT" base-url uri body :additional-headers additional-headers - :with-sni? with-sni?)) \ No newline at end of file diff --git a/src/clojure_mauth_client/util.clj b/src/clojure_mauth_client/util.clj deleted file mode 100644 index 1337369..0000000 --- a/src/clojure_mauth_client/util.clj +++ /dev/null @@ -1,37 +0,0 @@ -(ns clojure-mauth-client.util - (:require [clojure.string :refer [blank?]] - [pem-reader.core :as pem]) - (:use digest) - (:import (java.io ByteArrayInputStream))) - -(set! *warn-on-reflection* true) - -(defn epoch-seconds [] - (long (/ (System/currentTimeMillis) 1000))) - -(defn msg->sha512 [^String msg] - (-> msg - .getBytes - sha-512)) - -(defn get-uri [url] - (-> url - java.net.URL. - .getPath - (#(if (blank? %) "/" %)))) - -(defn read-key [^String key-str] - (-> key-str - .getBytes - ByteArrayInputStream. - pem/read)) - -(defn get-hex-encoded-digested-string [msg] - (msg->sha512 msg)) - -(defn str->bytes - "Convert string to byte array." - ([^String s] - (str->bytes s "UTF-8")) - ([^String s, ^String encoding] - (.getBytes s encoding))) diff --git a/src/clojure_mauth_client/validate.clj b/src/clojure_mauth_client/validate.clj deleted file mode 100644 index 52e1a94..0000000 --- a/src/clojure_mauth_client/validate.clj +++ /dev/null @@ -1,45 +0,0 @@ -(ns clojure-mauth-client.validate - (:require [clojure.data.json :as json] - [clojure.string :refer [trim]]) - (:use clojure-mauth-client.request - clojure-mauth-client.credentials - clojure.data.codec.base64)) - -(def ^{:private true} auth-uri "/mauth/v1/authentication_tickets.json") - -(defn- build-auth-ticket-body - [verb app-uuid request-url body time signature] - {:authentication_ticket - {:app_uuid app-uuid - :verb verb - :request_url request-url - :b64encoded_body (String. (encode (.getBytes body))) - :request_time time - :client_signature signature}}) - -(defn signature-map [msg] - (let [signature msg - values-fn (fn [s] (rest (re-find #"\A(\S+)\s*([^:]+):([^:]+)\z" s)))] - (if (nil? signature) {} - (zipmap [:token :app-uuid :signature] (values-fn (trim signature)))))) - -(defn validate! - ([verb uri body time signature] - (validate! verb uri body time signature nil)) - ([verb uri body time signature mauth-version] - (validate! verb uri body time signature mauth-version nil)) - ([verb uri body time signature mauth-version query-string] - (let [creds (get-credentials) - sig (signature-map signature) - auth-body (build-auth-ticket-body verb (:app-uuid sig) uri body time (:signature sig)) - updated-body (cond-> auth-body - (= mauth-version "v2") (assoc-in [:authentication_ticket :token] (:token sig)) - (and (= mauth-version "v2") query-string) (assoc-in [:authentication_ticket :query_string] query-string)) - auth-ticket-body (json/write-str updated-body)] - (-> - (post! (:mauth-service-url creds) - auth-uri - auth-ticket-body - :additional-headers {"Content-Type" "application/json"}) - :status - (= 204))))) diff --git a/test/clojure_mauth_client/header_test.clj b/test/clojure_mauth_client/header_test.clj deleted file mode 100644 index c05b2dc..0000000 --- a/test/clojure_mauth_client/header_test.clj +++ /dev/null @@ -1,55 +0,0 @@ -(ns clojure-mauth-client.header-test - (:require [clojure.test :refer :all] - [clojure-mauth-client.header :as header] - [clojure-mauth-client.util :as util] - [clojure-mauth-client.credentials :as credentials])) - -(use-fixtures - :once - (fn [f] - ;Note: these are NOT valid real credentials. - (credentials/define-credentials "abcd7d78-c874-47d9-a829-ccaa51ae75c9" - "-----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAsaa4gcNl4jx9YF7Y/6B+v29c0KBs1vxym0p4hwjZl5GteQgR - uFW5wM93F2lUFiVEQoM+Ti3AQjEDWdeuOIfo66LgbNLH7B3JhbkwHti/bMsq7T66 - Gs3cOhMcKDrTswOv8x72QzsOf1FNs7Yzsu1iwJpttNg+VCRj169hQ/YI39KSuYzQ - aXdjK0++EPsFtr2fU7E4cHGCDlAr8Tgt2gY8xmIuLF513jqJ2fhurja+YZriGwuO - qdBDLVpJOB1iz8bQ1CGMMGbkYl64jfsMHMBeueP9RyZ50I2Jrr8v05qG2dYDeQYn - d7BjAy2VLiWewRFQRltMOWC3nbZAla+282J/swIDAQABAoIBAAvPfrK5z9szlE5E - 3/5WqDaH686+6517WQ8z60Fm+DhYagUC4VK0+E12PX+j9AAo6BnX6dt+tSpxYbym - VyHQ/04zHOJ/POVYsZ4fSrCyTj+oXik5o1vG1d5SiOuvxYVAOIFcTJj5oyQZvqW0 - 9kjt+UO+wI5mVfZ4GN8s/LVs9PgURmYiSzy9Ed43kc4p9pSLQ448f7mnv134WknT - l/2z5/+q0DXj2Nx/JV0OsTR5KV4b0u87aSNrRldcKnfOWK9BubP4T9yGMjPyeuZP - XA0UQMRlGpql8I+4rRvdHio+N2lpHsFDzd7ckmjWMjXdeU2dGhE6580YbEpuLQRb - SN86ylkCgYEA3d/0W2S4wtmbV4X9AhLDe4vWLP1GmMcmcZ2wwuljEK14xNdeYzyD - z42PZ6RsGtAtdUHe6/e+7TRpz/f938f4ygawCV7zXkF65bPm54zz9o7+CM9p4P3V - AntDVO5IskiE6hb5A7wR0UdRNLDhov38Ngh+iUEDJ9MuZ75GWj6UbGcCgYEAzPmE - om9eaDC3g2hmBwTB54bAMgskRT9sdaFFJtMHx7VU0xw9E34qfD17o0LDa9n62TDv - getytWnluzwyhRw6UPhzseQw6R+7wtLlLPAj9mC58l34ONXwPSME0Exp/9WAVErt - Z0Ng6xPmifesQ/fSUM52aCIgufyQOQ1zCx9ogtUCgYBotpOKtqSEQVMRIYlg+x4L - Jtnz7azt2b+JC5UqyB8a9ePzcnl3eE31HKg7j9v9Y5awql/dGdWf+Yaewjms7aG7 - JyDZq1hMebbYxekKCvnwuVenLMyZhPKM80O5x6PDkHo6SJFJc+8sx+3JYll7JUds - 8OFXQbmNiBt0ltZ5LOO7rQKBgB1/HrYdXrGRqSbw5BXIenrt6kSJU+vfJ6V50rC2 - l50GnDFRE/z1H/oHAv7IgcTIdo/AugaxMi2nEpcyH3cGS+IRDt0foGY72dI8dRxV - ZmdzHe8h1LGhH9Q8cNnk1TAqsi/vJGDC0nShxYA/MvwI8qwMOf/cQWdiUALVy6Nj - HrANAoGBAIK6Lh0mZLtan60F1rUoXqME7yIGQgf/cP+RKLwE36kqF5DQx9aFwPZx - 9aWuKH+/XjdHf/J1n/AQ1j/G/WExs3UNfrvDgYea5QDnvc2gMBDRkdBwFZHYZLIn - e+viqMbgmORJDP/8vbpd0yZjT25ImysJE5cSCGiqHOotDs3jdlUX - -----END RSA PRIVATE KEY-----" - "https://mauth-sandbox.imedidata.net") - (with-redefs [util/epoch-seconds (fn [] 1532825948)] - (f)))) - -(deftest header-test - (testing "It should generate a valid mauth X-MWS header for GET" - (let [creds (credentials/get-credentials) - mauth-headers (header/build-mauth-headers "GET" "https://www.mdsol.com/api/v2/testing" "" (:app-uuid creds) (:private-key creds))] - (is (= {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:gI/yUeSTbiOWggLvCv2IJP19GFvmlE8RoaUrIpyLE8DY/mCQd8CUPgT9xNHGNqgPGe9f4CZdiFCC79Xvp6seZAq8/CnqA1dsJW6f46scqqTs+4N1TJml6GNCT9xU4tjUyHWFWpCBQlSvpoTFsLSq2d2zas9M9q1sgwPBS/oPGEN1agCQLHZS/Ime4ub8MuXh0Q8aWodqCpVi4GPiap/KLIQEzbvhsdayxmAcs2XDjpt+CReRf3tBCzB1RucVEfBehxtDQGgvrs/UCUbkpq7gY7f2k0RkrH+IopfhYfdNpmCHW12OEQoZ74TVbh61Uo+xcD1der46+tWk0mdnlyXKow==" - "X-MWS-Time" "1532825948"} mauth-headers)))) - - (testing "It should generate a valid mauth X-MWS header for response" - (let [creds (credentials/get-credentials) - mauth-headers (header/build-mauth-headers 200 "{\"test\":\"testing\"}" (:app-uuid creds) (:private-key creds))] - (is (= {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:i0rLrgEN8Jy/M4kA1PNNckUxeGU2pL3PGCOjdhRrC6egHVTvNfJLXveVG/7wAU9H4hpLYsThyV1/LRc/OupBZmKYRDzqD6OneZVLkysehk6/OHKb8j8uJQnBSLB72ooIPPIUxJqtasHCi6cdzkBEZf3omp7qzkinhuX2Wi/t70xfC5YmeTydBoe2d+zcIDJZb6+zON4V5CwGMPlCLK6iFD8+hk9ddhszQZ+siHK8SWxrhrMGRDN9xyp3ljw+f62tlpTZi0KEHAr0M/aq49aP3Iv/XrN88Cl5Tt1nGIWslarfJrAxFNfzofO0KULqFB9nRV1NkBbrSMyjGvea7nGNBw==" - "X-MWS-Time" "1532825948"} mauth-headers))))) - diff --git a/test/clojure_mauth_client/header_v2_test.clj b/test/clojure_mauth_client/header_v2_test.clj deleted file mode 100644 index 9210be3..0000000 --- a/test/clojure_mauth_client/header_v2_test.clj +++ /dev/null @@ -1,54 +0,0 @@ -(ns clojure-mauth-client.header-v2-test - (:require [clojure.test :refer :all] - [clojure-mauth-client.header-v2 :as header-v2] - [clojure-mauth-client.credentials :as credentials] - [clojure-mauth-client.util :as util])) - -(use-fixtures - :once - (fn [f] - ;Note: these are NOT valid real credentials. - (credentials/define-credentials "abcd7d78-c874-47d9-a829-ccaa51ae75c9" - "-----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAsaa4gcNl4jx9YF7Y/6B+v29c0KBs1vxym0p4hwjZl5GteQgR - uFW5wM93F2lUFiVEQoM+Ti3AQjEDWdeuOIfo66LgbNLH7B3JhbkwHti/bMsq7T66 - Gs3cOhMcKDrTswOv8x72QzsOf1FNs7Yzsu1iwJpttNg+VCRj169hQ/YI39KSuYzQ - aXdjK0++EPsFtr2fU7E4cHGCDlAr8Tgt2gY8xmIuLF513jqJ2fhurja+YZriGwuO - qdBDLVpJOB1iz8bQ1CGMMGbkYl64jfsMHMBeueP9RyZ50I2Jrr8v05qG2dYDeQYn - d7BjAy2VLiWewRFQRltMOWC3nbZAla+282J/swIDAQABAoIBAAvPfrK5z9szlE5E - 3/5WqDaH686+6517WQ8z60Fm+DhYagUC4VK0+E12PX+j9AAo6BnX6dt+tSpxYbym - VyHQ/04zHOJ/POVYsZ4fSrCyTj+oXik5o1vG1d5SiOuvxYVAOIFcTJj5oyQZvqW0 - 9kjt+UO+wI5mVfZ4GN8s/LVs9PgURmYiSzy9Ed43kc4p9pSLQ448f7mnv134WknT - l/2z5/+q0DXj2Nx/JV0OsTR5KV4b0u87aSNrRldcKnfOWK9BubP4T9yGMjPyeuZP - XA0UQMRlGpql8I+4rRvdHio+N2lpHsFDzd7ckmjWMjXdeU2dGhE6580YbEpuLQRb - SN86ylkCgYEA3d/0W2S4wtmbV4X9AhLDe4vWLP1GmMcmcZ2wwuljEK14xNdeYzyD - z42PZ6RsGtAtdUHe6/e+7TRpz/f938f4ygawCV7zXkF65bPm54zz9o7+CM9p4P3V - AntDVO5IskiE6hb5A7wR0UdRNLDhov38Ngh+iUEDJ9MuZ75GWj6UbGcCgYEAzPmE - om9eaDC3g2hmBwTB54bAMgskRT9sdaFFJtMHx7VU0xw9E34qfD17o0LDa9n62TDv - getytWnluzwyhRw6UPhzseQw6R+7wtLlLPAj9mC58l34ONXwPSME0Exp/9WAVErt - Z0Ng6xPmifesQ/fSUM52aCIgufyQOQ1zCx9ogtUCgYBotpOKtqSEQVMRIYlg+x4L - Jtnz7azt2b+JC5UqyB8a9ePzcnl3eE31HKg7j9v9Y5awql/dGdWf+Yaewjms7aG7 - JyDZq1hMebbYxekKCvnwuVenLMyZhPKM80O5x6PDkHo6SJFJc+8sx+3JYll7JUds - 8OFXQbmNiBt0ltZ5LOO7rQKBgB1/HrYdXrGRqSbw5BXIenrt6kSJU+vfJ6V50rC2 - l50GnDFRE/z1H/oHAv7IgcTIdo/AugaxMi2nEpcyH3cGS+IRDt0foGY72dI8dRxV - ZmdzHe8h1LGhH9Q8cNnk1TAqsi/vJGDC0nShxYA/MvwI8qwMOf/cQWdiUALVy6Nj - HrANAoGBAIK6Lh0mZLtan60F1rUoXqME7yIGQgf/cP+RKLwE36kqF5DQx9aFwPZx - 9aWuKH+/XjdHf/J1n/AQ1j/G/WExs3UNfrvDgYea5QDnvc2gMBDRkdBwFZHYZLIn - e+viqMbgmORJDP/8vbpd0yZjT25ImysJE5cSCGiqHOotDs3jdlUX - -----END RSA PRIVATE KEY-----" - "https://mauth-sandbox.imedidata.net") - (with-redefs [util/epoch-seconds (fn [] 1532825948)] - (f)))) - -(deftest header-v2-test - (testing "It should generate a valid mauth MWSV2 header for POST" - (let [creds (credentials/get-credentials) - mauth-header-v2 (header-v2/build-mauth-headers-v2 "POST" "https://www.mdsol.com/api/v2/testing" "" (:app-uuid creds) (:private-key creds) "abc=testing&test=1234")] - (is (= {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:nB0sLA13YT3n5HYeuox4iiGTPNrxS0XsgWRNIC51U3Pd0FFTBlbHVO+5URTywg5BAmC3hSruFP6S116ewjJE2lUoNdXSp9gsRt6GZ6Y/QGrd+U54TWKuk13rumiphljb4HdslAOe17ac6nqRxqF/Rd16lK/0nvTpPN4Z1USJ0L0YwJDUpkhaaTsvt6LIeIZjY6wexikZK/q4CvXQDwRmhtdaTICdAMjvbhJ6apnR3e8Ra9I9wmb713DudcBn2uerHuURMywTkqUCZMcH4cF3a8Hi6Lpm85BfzP+9RRzcUJbU5hkzIor58hrsvJv/9WVFGrt8JExnmcWKrgA/rj2X2g==;" - "mcc-time" "1532825948"} mauth-header-v2)))) - - (testing "It should generate a valid mauth MWSV2 header for response" - (let [creds (credentials/get-credentials) - mauth-headers-v2 (header-v2/build-mauth-headers-v2 200 "{\"test\":\"testing\"}" (:app-uuid creds) (:private-key creds))] - (is (= {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:pTk0Kfs6utta/wixDIwmycbPR5MMPKkU5ETZ+SlUlAMIx2o3sJxMc10ezfdO+z34l47yPIvyFytE+StMkJiLQMDpA9eKl2Xek3QdYL5YBZlG6j1b2lVgF0ptWqxXvQXlv7ets9K6BZ/HGEYj3/3IXzeNRNKYrDCTtGv8JSjdqqpLxSFMlpB++MkjXNlTh2AwIQua5ZTTSpcw8lgxA9PgXQPDeT88bqx2jhsfELG3IG3Sn9DVPOzprsicpVljYHzqjb1+wtNCaD3PYBrkDQZeVd9Y9jU2Hy6CdjnScFQvZlqg043GKQY4OJGvxVUsJDkQHkb7jzLjXfypcvvXHulWDA==;" - "mcc-time" "1532825948"} mauth-headers-v2))))) \ No newline at end of file diff --git a/test/clojure_mauth_client/middleware_test.clj b/test/clojure_mauth_client/middleware_test.clj deleted file mode 100644 index 9501b9b..0000000 --- a/test/clojure_mauth_client/middleware_test.clj +++ /dev/null @@ -1,82 +0,0 @@ -(ns clojure-mauth-client.middleware-test - (:require [clojure-mauth-client.middleware :as middleware] - [clojure.test :refer [deftest testing is]] - [clojure-mauth-client.validate :refer [validate!]] - [clojure-mauth-client.request :refer [post!]] - [clojure-mauth-client.credentials :refer [get-credentials]])) - -(def mock-post-request-v2 - {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:T0XZu8X6bUcKBW/QgX0RnUg0hfbcDfm==;" - "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} - :url "https://www.mdsol.com/api/v2/testing" - :request-method :post - :body "\"{\"test\":{\"request\":123}}\""}) - -(def mock-post-request-v1 - {:headers {"x-mws-authentication" "MWS a5a733c5-2bae-400c-aae9-6bb5b99d4130:SpjzqMFJ0cl8Lvi72TcU1qfVP9rzRWH/Jys2g==;" - "x-mws-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} - :url "https://www.mdsol.com/api/v1/testing" - :request-method :post - :body "\"{\"test\":{\"request\":123}}\""}) - -(def mock-post-request-without-mauth-headers - {:headers {:Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} - :url "https://www.mdsol.com/api/v1/testing" - :request-method :post - :body "\"{\"test\":{\"request\":123}}\""}) - -(def mock-post-request-v2-with-invalid-signature - {:headers {"mcc-authentication" "MWSV abcd7d78-c874-47d9-a829-ccaa51ae75c9:T0XZu8X6bUcKBW/QgX0RnUg0hfbcDfm==;" - "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} - :url "https://www.mdsol.com/api/v2/testing" - :request-method :post - :body "\"{\"test\":{\"request\":123}}\""}) - -(defn mock-handler [{:keys [body] :as request}] - {:body body - :status 200}) - -(deftest test-wrap-mauth-verification - (testing "Request should get validated successfully" - (with-redefs [validate! (fn [& _] (constantly true))] - (let [request-function (middleware/wrap-mauth-verification mock-handler) - {:keys [status body]} (request-function mock-post-request-v2)] - (is (= 200 status)) - (is (= "\"{\"test\":{\"request\":123}}\"" body))))) - - (testing "Request should get invalidated and should return Unauthorized with 401 error code with v2" - (with-redefs [validate! (fn [& _] false)] - (let [request-function (middleware/wrap-mauth-verification mock-handler) - {:keys [status body]} (request-function mock-post-request-v2)] - (is (= 401 status)) - (is (= "Unauthorized." body))))) - - (testing "Request should get invalidated and should return Unauthorized with 401 error code with v1" - (with-redefs [validate! (fn [& _] false)] - (let [request-function (middleware/wrap-mauth-verification mock-handler) - {:keys [status body]} (request-function mock-post-request-v1)] - (is (= 401 status)) - (is (= "Unauthorized." body))))) - - (testing "Request should get validated as per v2 version" - (with-redefs [post! (fn [& args] - {:status 204}) - get-credentials (constantly {:mauth-service-url "http://test.com"})] - (let [request-function (middleware/wrap-mauth-verification mock-handler) - {:keys [status body]} (request-function mock-post-request-v2)] - (is (= 200 status))))) - - (testing "Request should get validated as per v1 version" - (with-redefs [post! (fn [& args] - {:status 204}) - get-credentials (constantly {:mauth-service-url "http://test.com"})] - (let [request-function (middleware/wrap-mauth-verification mock-handler) - {:keys [status body]} (request-function mock-post-request-v1)] - (is (= 200 status)))))) diff --git a/test/clojure_mauth_client/request_test.clj b/test/clojure_mauth_client/request_test.clj deleted file mode 100644 index 9529cf1..0000000 --- a/test/clojure_mauth_client/request_test.clj +++ /dev/null @@ -1,218 +0,0 @@ -(ns clojure-mauth-client.request-test - (:require [clojure.test :refer :all] - [clojure-mauth-client.request :refer :all] - [clojure.data.json :as json] - [clojure-mauth-client.util :as util]) - (:use [clojure-mauth-client.credentials])) - -(use-fixtures - :once - (fn [f] - ;Note: these are NOT valid real credentials. - (define-credentials "abcd7d78-c874-47d9-a829-ccaa51ae75c9" - "-----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAsaa4gcNl4jx9YF7Y/6B+v29c0KBs1vxym0p4hwjZl5GteQgR - uFW5wM93F2lUFiVEQoM+Ti3AQjEDWdeuOIfo66LgbNLH7B3JhbkwHti/bMsq7T66 - Gs3cOhMcKDrTswOv8x72QzsOf1FNs7Yzsu1iwJpttNg+VCRj169hQ/YI39KSuYzQ - aXdjK0++EPsFtr2fU7E4cHGCDlAr8Tgt2gY8xmIuLF513jqJ2fhurja+YZriGwuO - qdBDLVpJOB1iz8bQ1CGMMGbkYl64jfsMHMBeueP9RyZ50I2Jrr8v05qG2dYDeQYn - d7BjAy2VLiWewRFQRltMOWC3nbZAla+282J/swIDAQABAoIBAAvPfrK5z9szlE5E - 3/5WqDaH686+6517WQ8z60Fm+DhYagUC4VK0+E12PX+j9AAo6BnX6dt+tSpxYbym - VyHQ/04zHOJ/POVYsZ4fSrCyTj+oXik5o1vG1d5SiOuvxYVAOIFcTJj5oyQZvqW0 - 9kjt+UO+wI5mVfZ4GN8s/LVs9PgURmYiSzy9Ed43kc4p9pSLQ448f7mnv134WknT - l/2z5/+q0DXj2Nx/JV0OsTR5KV4b0u87aSNrRldcKnfOWK9BubP4T9yGMjPyeuZP - XA0UQMRlGpql8I+4rRvdHio+N2lpHsFDzd7ckmjWMjXdeU2dGhE6580YbEpuLQRb - SN86ylkCgYEA3d/0W2S4wtmbV4X9AhLDe4vWLP1GmMcmcZ2wwuljEK14xNdeYzyD - z42PZ6RsGtAtdUHe6/e+7TRpz/f938f4ygawCV7zXkF65bPm54zz9o7+CM9p4P3V - AntDVO5IskiE6hb5A7wR0UdRNLDhov38Ngh+iUEDJ9MuZ75GWj6UbGcCgYEAzPmE - om9eaDC3g2hmBwTB54bAMgskRT9sdaFFJtMHx7VU0xw9E34qfD17o0LDa9n62TDv - getytWnluzwyhRw6UPhzseQw6R+7wtLlLPAj9mC58l34ONXwPSME0Exp/9WAVErt - Z0Ng6xPmifesQ/fSUM52aCIgufyQOQ1zCx9ogtUCgYBotpOKtqSEQVMRIYlg+x4L - Jtnz7azt2b+JC5UqyB8a9ePzcnl3eE31HKg7j9v9Y5awql/dGdWf+Yaewjms7aG7 - JyDZq1hMebbYxekKCvnwuVenLMyZhPKM80O5x6PDkHo6SJFJc+8sx+3JYll7JUds - 8OFXQbmNiBt0ltZ5LOO7rQKBgB1/HrYdXrGRqSbw5BXIenrt6kSJU+vfJ6V50rC2 - l50GnDFRE/z1H/oHAv7IgcTIdo/AugaxMi2nEpcyH3cGS+IRDt0foGY72dI8dRxV - ZmdzHe8h1LGhH9Q8cNnk1TAqsi/vJGDC0nShxYA/MvwI8qwMOf/cQWdiUALVy6Nj - HrANAoGBAIK6Lh0mZLtan60F1rUoXqME7yIGQgf/cP+RKLwE36kqF5DQx9aFwPZx - 9aWuKH+/XjdHf/J1n/AQ1j/G/WExs3UNfrvDgYea5QDnvc2gMBDRkdBwFZHYZLIn - e+viqMbgmORJDP/8vbpd0yZjT25ImysJE5cSCGiqHOotDs3jdlUX - -----END RSA PRIVATE KEY-----" - "https://mauth-sandbox.imedidata.net") - - (with-redefs [util/epoch-seconds (fn [] 1532825948) - clj-http.client/request (fn [{:keys [client timeout filter worker-pool keepalive as follow-redirects max-redirects response - trace-redirects allow-unsafe-redirect-methods proxy-host proxy-port tunnel?] - :as opts - :or {client nil - timeout 60000 - follow-redirects true - max-redirects 10 - filter nil - worker-pool nil - response (promise) - keepalive 120000 - as :auto - tunnel? false - proxy-host nil - proxy-port 3128}} - & [callback]] opts)] - (f)))) - -(def mock-get - {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:gI/yUeSTbiOWggLvCv2IJP19GFvmlE8RoaUrIpyLE8DY/mCQd8CUPgT9xNHGNqgPGe9f4CZdiFCC79Xvp6seZAq8/CnqA1dsJW6f46scqqTs+4N1TJml6GNCT9xU4tjUyHWFWpCBQlSvpoTFsLSq2d2zas9M9q1sgwPBS/oPGEN1agCQLHZS/Ime4ub8MuXh0Q8aWodqCpVi4GPiap/KLIQEzbvhsdayxmAcs2XDjpt+CReRf3tBCzB1RucVEfBehxtDQGgvrs/UCUbkpq7gY7f2k0RkrH+IopfhYfdNpmCHW12OEQoZ74TVbh61Uo+xcD1der46+tWk0mdnlyXKow==" - "X-MWS-Time" "1532825948"} - :url "https://www.mdsol.com/api/v2/testing" - :method :get, - :body "\"\"", - :throw-exceptions false - :as :auto}) - -(def mock-get-with-qs - {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:gI/yUeSTbiOWggLvCv2IJP19GFvmlE8RoaUrIpyLE8DY/mCQd8CUPgT9xNHGNqgPGe9f4CZdiFCC79Xvp6seZAq8/CnqA1dsJW6f46scqqTs+4N1TJml6GNCT9xU4tjUyHWFWpCBQlSvpoTFsLSq2d2zas9M9q1sgwPBS/oPGEN1agCQLHZS/Ime4ub8MuXh0Q8aWodqCpVi4GPiap/KLIQEzbvhsdayxmAcs2XDjpt+CReRf3tBCzB1RucVEfBehxtDQGgvrs/UCUbkpq7gY7f2k0RkrH+IopfhYfdNpmCHW12OEQoZ74TVbh61Uo+xcD1der46+tWk0mdnlyXKow==" - "X-MWS-Time" "1532825948"} - :url "https://www.mdsol.com/api/v2/testing?testABCD=1234" - :method :get, - :body "\"\"", - :throw-exceptions false - :as :auto}) - -(def mock-post - {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:kYZR1udBwE02ct55cSWq5MZuGsC6xgVmK6TWYQ/+2IAbhqG7jZWhP95bPxTqo1f12XUWsX/oeUAp8jhvdUcsXsjVMeBwvQNgnC/HP1TNQasC2LMGfOs76WnsfKoV5zWDh+SNqMqn4pIXce3DALG9d/FB2Uu4mIg9kgQIUnfJJDljfLMjR7aMgDINPU7ToM51TqTJh+ReG7LAVwsEzSwFfj4zZFFpx8XrWn3inx5ZUvT7YcFhW4vOZaeI48HSnj80bCf1LDtWXzU7yk9gon+AlIYBTtrPQTSqyofBGZUtZCBexnoEp6NUhk5XkwqK56jizT7PZCN094kh1eofr3hSTg==" - "X-MWS-Time" "1532825948"}, - :url "https://www.mdsol.com/api/v2/testing1" - :method :post - :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" - :throw-exceptions false - :as :auto}) - -(def mock-delete - {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:IzAzkGyzDtxbMlCbbWViYen/9o54B9Ijlnp1UoSIysGr/axJWwph8KRYukS+3DhYFJIBLbS1PfWI74kRTkWJl3Vmb0XgxiRfNCqresqh687ELlhNDt66p2mu/6LaVwbDKUBsIAwkQFomVfAOy3jckWZjHRySD+VABfDf4BAf5hfjTUgil63oOnH6xII51e6M160SFRz1/HpsMU/rnReniPJs22MwiqS6dhe3oU/DAzteawxujSdFA3i6Fol6kdJQN19w+0TTdOSbccjds1Wljqu/+E1ju1rXVAgcL0GuVg4dsCwrjSPY9VWfQOttpA4aHavGWNcPMh1p1kSmqlNa1g==" - "X-MWS-Time" "1532825948"} - :url "https://www.mdsol.com/api/v2/testing2" - :method :delete - :body "\"\"", - :throw-exceptions false - :as :auto}) - -(def mock-put - {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:W+hIOQKAp0aEwDthAVaMa5ysJB8ddQdJdTWNonQoDuPEBVAY7F6GUXNoAZCYcosxgbm2rfpwyfLrS7U5b77GMFpnvvUwUSCgRziZNvfhuZfWUuW9po7OkWQUXCDvd/NtJdLOu6o1bKCGHYKjdaw/8AVH876afGyPF7+Ce2vD+YFRfY+zXF0MVWiS2WfwUwLSdOXb+Csnb21XT59zDs8qBg0gUj6WagZiJ+hYTbAt1zcNCdqs/mVt5hKA7ASxB9VY7GI4QM/n0EoyC/ruUm8DYS7kkxuxKeZuNkvpexFR4IPXQax1q7EtCIgw4yekegK210uxxYoOf+EBV2wMIVpKvw==" - "X-MWS-Time" "1532825948"} - :url "https://www.mdsol.com/api/v2/testing3" - :method :put - :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" - :throw-exceptions false - :as :auto}) - -(def mock-post-v2 - {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:EHrD5j5crMQ2Gv8YrEuZJKpeh0glru9ze+A2a4PoPDVyqz5DSJYmrREqRHVhIn3PrRUwFSECk+cMOH1IMOoiEe0nRg0nRWyZkbPDXfVpnQoq9cZXCmUSGCe5q8WHfqgu+gJCuXflI7n84QAFjxo7Iq+wl8oNC/RSqyqTWXPMF3dtIjiumo5091xhXyqEsUodvOtnYImvTX75pohyHjrrSdsWuVltQmQWjTzYeP4zKNmvznXVMG30BwLLuT97r5/NiyV2h9RbAMyxkGxKzcE56q8e7gddflIQGD/dgxHedcoihCzm5yHSqRFzx/X80LhCSWPlrcCBIUPGDREkUKbFwg==;" - "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" - :mauth-version "v2"} - :url "https://www.mdsol.com/api/v2/testing1" - :method :post - :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" - :throw-exceptions false - :as :auto}) - -(def mock-put-v2 - {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:nBKyoJMY5qvumQhLhP0aZr2xe+058aaMuWeP8+fH3MDvxCJTectJsVPO/17wgCwP14yG/EqHK2z8/SrEjMADiOM4QtCTPgcByonCuZzcWW+zSncpssLB3ItC8K7OJ9/urZ60wOxu+0v3Nhl+jYzrVB8fIxq3HELxxIhrq1Bt41BdLNNUTBR2RG6cB2cCHB5sw2bScC1BiuwS73zOkP59Q2uRpsCfdYjWj+u9WvDV1oakUflfjaZHMqsyGMgtMMl6idkR4OejyElyQ/gv4i0dPamR4m+VogMPnlQKyGsqRyXYu1Eo8jpQR+8Mu1pa9Xyu45v18jhOJPlLpF2/1HCp2w==;" - "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" - :mauth-version "v2"} - :url "https://www.mdsol.com/api/v2/testing1" - :method :put - :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" - :throw-exceptions false - :as :auto}) - -(def mock-get-v2 - {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:fdWPNlo6rTdMKxw5wf0KpQMKm22Zuf6PIUQRwIYtyzZgNEgJjgjlVdQogSYHTBVuLMx1iuyXH9m/Mex0jzD0DFZh/YIBRQ2Mn1ChKE5T0ejSyLjaTGHFg6sIpCWxVZzUvPKZKTDDdtk12vovGcRWBTfY62LpJpjlcI1YMEwcCL5A7fgIwwX9pxQO/AVJUDA9cowbBpDIjJlo9ZdcdBWfLcKfgPoWtEO5UDpvZZti0rSTiRLY3a0/l8xPLpuQXq9EjUrac0srunEK6te4XQ9MuhEQRYRkTIMF4Hvqxz9HYtkByuvpiSqNfXKCGXGxYhfAFVvRUhmk2RagISwj4qzbUA==;" - "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" - :mauth-version "v2"} - :url "https://www.mdsol.com/api/v2/testing1" - :method :get - :body "\"\"" - :throw-exceptions false - :as :auto}) - -(def mock-get-with-qs-v2 - {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:fdWPNlo6rTdMKxw5wf0KpQMKm22Zuf6PIUQRwIYtyzZgNEgJjgjlVdQogSYHTBVuLMx1iuyXH9m/Mex0jzD0DFZh/YIBRQ2Mn1ChKE5T0ejSyLjaTGHFg6sIpCWxVZzUvPKZKTDDdtk12vovGcRWBTfY62LpJpjlcI1YMEwcCL5A7fgIwwX9pxQO/AVJUDA9cowbBpDIjJlo9ZdcdBWfLcKfgPoWtEO5UDpvZZti0rSTiRLY3a0/l8xPLpuQXq9EjUrac0srunEK6te4XQ9MuhEQRYRkTIMF4Hvqxz9HYtkByuvpiSqNfXKCGXGxYhfAFVvRUhmk2RagISwj4qzbUA==;" "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" - :mauth-version "v2"} - :url "https://www.mdsol.com/api/v2/testing1?testABCD=1234" - :method :get - :body "\"\"" - :throw-exceptions false - :as :auto}) - -(def mock-delete-v2 - {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:f2qLS57HqU7ZoJx5hlhIt9nyjbB/tqiOwcWe3Y5lO7OLL2OKuR3Et8nF5SNEu4ToWrr67nu/16ztdNRC0138uRVVEdIhnPgD3z9WZZehHaoP64BHBEcleP6MBFVJ2p/9nJvqjvZ64qAkovzQf6lGQdBSp93X8MrDN/BMSxSGOUUXffPst4nzzl09dhgCCnk0vqHG8/wDELi2I5ieCxt3WVMDj+rcffm+C0e6Oc7qT4WVXYwxnVOSOZRWo7cpd5dEXTfG0KSJK47Zdoy/qlrhSY4byk+qwky57lROMixNxdeE5PNTh6qmvvvZWRVpb+vKJraP849eacgPEyBIvAezwA==;" - "mcc-time" "1532825948" - :Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" - :mauth-version "v2"} - :url "https://www.mdsol.com/api/v2/testing1" - :method :delete - :body "\"\"" - :throw-exceptions false - :as :auto}) - -(def additional-headers {:Content-Type "application/json" - :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" - :mauth-version "v2"}) - -(def mock-payload (-> {:a {:b 123}} - json/write-str)) - -(deftest header-test - (get-credentials) - (testing "It should make a valid GET request." - (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing")] - (is (= mock-get get-response)))) - - (testing "It should make a valid GET request with a querystring." - (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing?testABCD=1234")] - (is (= mock-get-with-qs get-response)))) - - (testing "It should make a valid POST request." - (let [post-response (post! "https://www.mdsol.com" "/api/v2/testing1" mock-payload)] - (is (= mock-post post-response)))) - - (testing "It should make a valid DELETE request." - (let [delete-response (delete! "https://www.mdsol.com" "/api/v2/testing2")] - (is (= mock-delete delete-response)))) - - (testing "It should make a valid PUT request." - (let [put-response (put! "https://www.mdsol.com" "/api/v2/testing3" mock-payload)] - (is (= mock-put put-response)))) - - (testing "It should make a valid POST request with v2 mauth header" - (let [post-response (post! "https://www.mdsol.com" "/api/v2/testing1" mock-payload :additional-headers additional-headers - :with-sni? false)] - (is (= mock-post-v2 post-response)))) - - (testing "It should make a valid PUT request with v2 mauth header" - (let [put-response (put! "https://www.mdsol.com" "/api/v2/testing1" mock-payload :additional-headers additional-headers - :with-sni? false)] - (is (= mock-put-v2 put-response)))) - - (testing "It should make a valid GET request with v2 mauth header" - (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing1" :additional-headers additional-headers - :with-sni? false)] - (is (= mock-get-v2 get-response)))) - - (testing "It should make a valid GET request with query string and with v2 mauth header" - - (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing1?testABCD=1234" :additional-headers additional-headers - :with-sni? false)] - (is (= mock-get-with-qs-v2 get-response)))) - - (testing "It should make a valid DELETE request with v2 mauth header" - (let [delete-response (delete! "https://www.mdsol.com" "/api/v2/testing1" :additional-headers additional-headers - :with-sni? false)] - (is (= mock-delete-v2 delete-response))))) diff --git a/test/clojure_mauth_client/validate_test.clj b/test/clojure_mauth_client/validate_test.clj deleted file mode 100644 index 659da00..0000000 --- a/test/clojure_mauth_client/validate_test.clj +++ /dev/null @@ -1,63 +0,0 @@ -(ns clojure-mauth-client.validate-test - (:require [clojure.test :refer [deftest testing is]] - [clojure-mauth-client.validate :as mauth-validate :refer [validate!]] - [clojure-mauth-client.request :refer [post!]] - [clojure-mauth-client.credentials :refer [get-credentials]] - [clojure.data.json :as json])) - -(def ^:private request-data - {:verb (.toUpperCase (name :post)) - :url "https://www.mdsol.com/api/v1/testing" - :body "\"{\"test\":{\"request\":123}}\"" - :time "1532825948" - :v1-signature "MWS a5a733c5-2bae-400c-aae9-6bb5b99d4130:SpjzqMFJ0cl8Lvi72TcU1qfVP9rzRWH/Jys2g==;" - :v2-signature "MWSV2 a5a733c5-2bae-400c-aae9-6bb5b99d4130:T0XZu8X6bUcKBW/QgX0RnUg0hfbcDfm==;"}) - -(deftest test-validate-success - (with-redefs [post! (fn [& args] - {:status 204}) - get-credentials (constantly {:mauth-service-url "http://test.com"})] - - (testing "testing validate with mauth v1 version" - (let [{:keys [verb url body time v1-signature]} request-data] - (is (true? (validate! verb url body time v1-signature))))) - - (testing "testing validate with mauth v2 version" - (let [{:keys [verb url body time v2-signature]} request-data] - (is (true? (validate! verb url body time v2-signature "v2"))))))) - -(deftest test-validate-failure - (with-redefs [post! (fn [& args] - {:status 400}) - get-credentials (constantly {:mauth-service-url "http://test.com"})] - - (testing "testing validate with mauth v1 version" - (let [{:keys [verb url body time v1-signature]} request-data] - (is (false? (validate! verb url body time v1-signature))))) - - (testing "testing validate with mauth v2 version" - (let [{:keys [verb url body time v2-signature]} request-data] - (is (false? (validate! verb url body time v2-signature "v2"))))))) - -(deftest test-auth-ticket-body - (let [auth-ticket-atom (atom nil)] - (with-redefs [post! (fn [_ _ auth-ticket-body & _args] - (reset! auth-ticket-atom auth-ticket-body)) - get-credentials (constantly {:mauth-service-url "http://test.com"})] - (testing "V1 protocol" - (let [{:keys [verb url body time v1-signature]} request-data - _ (validate! verb url body time v1-signature) - auth-ticket (json/read-str @auth-ticket-atom :key-fn keyword)] - (is (= verb (get-in auth-ticket [:authentication_ticket :verb]))) - (is (= url (get-in auth-ticket [:authentication_ticket :request_url]))) - (is (nil? (get-in auth-ticket [:authentication_ticket :token]))) - (is (nil? (get-in auth-ticket [:authentication_ticket :query_string]))))) - (testing "V2 protocol" - (let [{:keys [verb url body time v2-signature]} request-data - query-string "color=black&model=mustang" - _ (validate! verb url body time v2-signature "v2" query-string) - auth-ticket (json/read-str @auth-ticket-atom :key-fn keyword)] - (is (= verb (get-in auth-ticket [:authentication_ticket :verb]))) - (is (= url (get-in auth-ticket [:authentication_ticket :request_url]))) - (is (= "MWSV2" (get-in auth-ticket [:authentication_ticket :token]))) - (is (= query-string (get-in auth-ticket [:authentication_ticket :query_string])))))))) From e92052f6aca2aa155f8ffdfce6e600b9ed20cfa2 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:12:28 -0400 Subject: [PATCH 04/32] Major version bump --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index d1d8b1a..f11bab5 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 646ad26326ed6ee384bf18637dd9fdfb275cd7f8 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:12:46 -0400 Subject: [PATCH 05/32] 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 343b70f..54043ea 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 094c3a15a7c3834ae27f54db52da09ddc0c7d503 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:17:37 -0400 Subject: [PATCH 06/32] 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 f11bab5..b490831 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"] @@ -15,6 +14,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 3467fb88050f73390837ad4963bd0a57d3cf4c69 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:17:43 -0400 Subject: [PATCH 07/32] 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 54043ea..b903169 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)))) @@ -76,5 +76,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 6eca893247d3308e590a61925e8ec0cb01ca1fd0 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 15:18:05 -0400 Subject: [PATCH 08/32] 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 b903169..33b6b10 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 42bad20fe09bc73571f16b9e9f33f792741f40f9 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Wed, 14 May 2025 19:20:59 -0400 Subject: [PATCH 09/32] 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 c66598975bc79e5abbaf280a770cc371be4f78c7 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 11:43:39 -0400 Subject: [PATCH 10/32] 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} | 63 ++++++++++++--- 5 files changed, 157 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 b490831..72f3acb 100644 --- a/project.clj +++ b/project.clj @@ -12,9 +12,11 @@ #_[clojure-interop/java.security "1.0.5"] #_[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 33b6b10..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,12 +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 809ae1b1744440b8b3bfcb8fe0ded016e21fc01b Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 11:47:48 -0400 Subject: [PATCH 11/32] 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 32f1e183b69688d32a1c2c2551bb974232621aba Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:28:09 -0400 Subject: [PATCH 12/32] 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 9b6e86b2905e03aa65ce0950e87fa5259e640ec2 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:36:12 -0400 Subject: [PATCH 13/32] Add humane-test-output --- project.clj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 72f3acb..715878f 100644 --- a/project.clj +++ b/project.clj @@ -16,7 +16,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 50fa0df0c86c2b4921c45f75bb3f192eee1aab1c Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:36:23 -0400 Subject: [PATCH 14/32] 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 f6ade281836ec349c4acf508a9e05c995e47caca Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:37:58 -0400 Subject: [PATCH 15/32] 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 00aa1145614a3bd77dc9021e8222e17b29438355 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:44:46 -0400 Subject: [PATCH 16/32] 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 6cdb622023963b6c495e7697a8dc0e6a52ca4298 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 12:45:25 -0400 Subject: [PATCH 17/32] 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 67183a374268bbd53ad8e30144a8d606dcb4ec6d Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:14:23 -0400 Subject: [PATCH 18/32] Remove commented out deps --- project.clj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/project.clj b/project.clj index 715878f..0416ca5 100644 --- a/project.clj +++ b/project.clj @@ -6,12 +6,6 @@ :dependencies [[camel-snake-kebab "0.4.3"] [com.cnuernber/charred "1.033"] [org.clojure/clojure "1.12.0"] - #_[xsc/pem-reader "0.1.1"] - #_[digest "1.4.10"] - #_[org.clojure/data.codec "0.1.1"] - #_[clojure-interop/java.security "1.0.5"] - #_[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"]] From a6bdecd538098f7f66b6b4f3cf99467ffb111425 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:32:34 -0400 Subject: [PATCH 19/32] 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 741bf631c17dce7355960eddc59d67efab96c536 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:36:05 -0400 Subject: [PATCH 20/32] 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 0889beec4e5b407d36e400a2919fe8cf80afcdff Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 13:39:08 -0400 Subject: [PATCH 21/32] 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 2ca247fb85aa9c701f31b2208eff3f405b913fc0 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:26:44 -0400 Subject: [PATCH 22/32] 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 40b115d5e9ff83cd3b509a56c8964870f7fec6e7 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:29:05 -0400 Subject: [PATCH 23/32] 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 8a5b62001f27dd4ee4db3a677195ca0357301827 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:29:53 -0400 Subject: [PATCH 24/32] 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 eba3665e31c3b313772150bac1603ffd11a0b683 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:33:40 -0400 Subject: [PATCH 25/32] 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 6cd4153bd7c92741aab5b120800810dbe648b092 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:44:09 -0400 Subject: [PATCH 26/32] 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 04f9004323d44b6f4c5e289b5dd655cd8e000199 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:45:33 -0400 Subject: [PATCH 27/32] 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 da8802ff959eb9c3aa38b3f4bfb5b5e89bc8493d Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:48:53 -0400 Subject: [PATCH 28/32] 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 a4ed6b9c70ea9c831e131971efc7b527730fb9a6 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 15:50:16 -0400 Subject: [PATCH 29/32] 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 ec8d54abb5f2572350b1c418a07ef0ebbf4d64cc Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 16:13:36 -0400 Subject: [PATCH 30/32] Remove unused dependency --- project.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/project.clj b/project.clj index 0416ca5..c60d447 100644 --- a/project.clj +++ b/project.clj @@ -10,7 +10,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 1cf347e944e0b05f3fa8df458f67145ebf7d2e46 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 16:32:51 -0400 Subject: [PATCH 31/32] 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 d4ba62fbbe500a65124f71af67efe6eda6c36d98 Mon Sep 17 00:00:00 2001 From: "Colin P. Hill" Date: Thu, 15 May 2025 16:55:04 -0400 Subject: [PATCH 32/32] 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