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 c86838e..c60d447 100644 --- a/project.clj +++ b/project.clj @@ -1,17 +1,22 @@ -(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" :url "https://opensource.org/licenses/MIT"} - :dependencies [[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"]] + :dependencies [[camel-snake-kebab "0.4.3"] + [com.cnuernber/charred "1.033"] + [org.clojure/clojure "1.12.0"] + [com.mdsol/mauth-authenticator "19.0.0"] + [com.mdsol/mauth-signer "19.0.0"]] + + :profiles {:test {:dependencies [[com.mdsol/mauth-authenticator-apachehttp "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 + :password :env/artifactory_password}]] :deploy-repositories [["releases" {:url "https://clojars.org/repo" @@ -26,23 +31,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/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/src/com/mdsol/mauth/clojure/authenticator.clj b/src/com/mdsol/mauth/clojure/authenticator.clj new file mode 100644 index 0000000..c37287a --- /dev/null +++ b/src/com/mdsol/mauth/clojure/authenticator.clj @@ -0,0 +1,117 @@ +(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.exception MAuthValidationException) + (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. + +(defn- ->string ^String [x] + (if (ident? x) + (name x) + (str x))) + +(defn- mauth-request ^MAuthRequest [request] + (let [{:keys [request-method uri body headers query-string]} request + java-uri (URI. (str uri \? query-string)) + [t b] (convert/->bytes-or-input-stream body)] + (-> (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 + "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 + 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? + "Returns `true` if the Ring request map passes `authenticator`'s validation." + [^Authenticator authenticator request] + (try + (.authenticate authenticator (mauth-request request)))) + +(defn default-on-auth-failure + "Returns a static map with a 401 response." + ([{:keys [exception]}] + {:status 401 + :body {:message (if exception + (ex-message exception) + "MAuth authentication failed.")}}) + ;; 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] + :or {on-auth-failure default-on-auth-failure}}] + (fn + ([request] + (try + (if (valid? authenticator request) + (handler request) + (on-auth-failure {:request request + :handler handler})) + (catch MAuthValidationException e + (on-auth-failure {:request request + :handler handler + :exception e})))) + ;; 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/convert.clj b/src/com/mdsol/mauth/clojure/convert.clj new file mode 100644 index 0000000..addc0cc --- /dev/null +++ b/src/com/mdsol/mauth/clojure/convert.clj @@ -0,0 +1,122 @@ +(ns com.mdsol.mauth.clojure.convert + (:require + [clojure.string :as str]) + (:import + (clojure.lang IDeref IFn Keyword) + (com.mdsol.mauth MAuthVersion) + (com.mdsol.mauth.util EpochTimeProvider) + (java.io + ByteArrayInputStream + CharArrayReader + InputStream + StringReader) + (java.util UUID) + [java.util.function IntSupplier LongSupplier Supplier])) + +(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))))) + 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`. + 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 new file mode 100644 index 0000000..d306453 --- /dev/null +++ b/src/com/mdsol/mauth/clojure/signer.clj @@ -0,0 +1,121 @@ +(ns com.mdsol.mauth.clojure.signer + (: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) + +(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 extending the protocols defined in + `com.mdsol.mauth.clojure.convert`." + ^DefaultSigner + [& {:keys [app-uuid private-key + epoch-time-provider sign-versions] + :or {epoch-time-provider current-epoch-time-provider + sign-versions [:mwsv2]}}] + (DefaultSigner. (convert/->uuid app-uuid) + ^String private-key + ^EpochTimeProvider (convert/->epoch-time-provider epoch-time-provider) + ^List (list* (map convert/->version sign-versions)))) + +(comment + (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----- +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-----"}))) + +(defn gen-req-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) + (name request-method) + ^String request-method) + [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 + ;; 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 + :stream (.generateRequestHeaders signer method uri + ^InputStream b + query-string) + :bytes (.generateRequestHeaders signer method uri + ^bytes b + query-string))))) + +(comment + (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)))) + ;; 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 + :headers {"content-type" "whatever"} + :uri "/foo" + :body "Hey hey"})) 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])))))))) diff --git a/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj new file mode 100644 index 0000000..68f85b8 --- /dev/null +++ b/test/com/mdsol/mauth/clojure/signer_authenticator_test.clj @@ -0,0 +1,203 @@ +(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.exception MAuthValidationException) + (com.mdsol.mauth.util MAuthKeysHelper) + (com.mdsol.mauth.utils ClientPublicKeyProvider) + (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] + (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) + ;; 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. + #_#_: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 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-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-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))) + +(defn norm-headers [m] + (-> m + (update-keys str/lower-case) + (update-vals str) + vec + (->> (sort-by first)))) + +(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))) + +(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 + ((auth/wrap-handler identity authenticator) + (update req :headers + merge headers))) + "Server middleware passes through on success")) + (is (= {:status 401 + :body "oops!" + ::got-request true} + ((auth/wrap-handler identity authenticator + {:on-auth-failure + (fn [{:keys [request exception handler]}] + (is (nil? exception)) + (is (= identity handler)) + {:status 401 + :body "oops!" + ::got-request (::is-request 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!" + ::got-request true} + ((auth/wrap-handler identity authenticator + {:on-auth-failure + (fn [{:keys [request exception handler]}] + (is (instance? MAuthValidationException + exception)) + (is (= identity handler)) + {:status 401 + :body "oops!" + ::got-request (::is-request request)})}) + (-> (request-fn) + (update :headers merge headers) + (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") + (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)))) + "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 + `(deftest ~(-> test-cases-v2 + (nth i) + :name + (->> (str "mwsv2-")) + symbol) + (validate-test-case (nth test-cases-v2 ~i) signer-v2)))) + +(doseq [i (range (count test-cases-v1))] + (eval + `(deftest ~(-> test-cases-v1 + (nth i) + :name + (->> (str "mws-")) + symbol) + (validate-test-case (nth test-cases-v1 ~i) signer-v1))))