├── .gitignore ├── src └── clojure_mauth_client │ ├── util.clj │ ├── credentials.clj │ ├── header.clj │ ├── header_v2.clj │ ├── validate.clj │ ├── middleware.clj │ └── request.clj ├── .travis.yml ├── LICENSE.md ├── project.clj ├── test └── clojure_mauth_client │ ├── validate_test.clj │ ├── header_test.clj │ ├── middleware_test.clj │ ├── header_v2_test.clj │ └── request_test.clj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | *.iml 13 | .idea/ 14 | tests.edn 15 | /.lsp/.cache 16 | /.clj-kondo/.cache 17 | /secrets/ 18 | .vscode/ 19 | /.calva 20 | -------------------------------------------------------------------------------- /src/clojure_mauth_client/util.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.util 2 | (:require [clojure.string :refer [blank?]] 3 | [pem-reader.core :as pem]) 4 | (:use digest) 5 | (:import (java.io ByteArrayInputStream))) 6 | 7 | (set! *warn-on-reflection* true) 8 | 9 | (defn epoch-seconds [] 10 | (long (/ (System/currentTimeMillis) 1000))) 11 | 12 | (defn msg->sha512 [^String msg] 13 | (-> msg 14 | .getBytes 15 | sha-512)) 16 | 17 | (defn get-uri [url] 18 | (-> url 19 | java.net.URL. 20 | .getPath 21 | (#(if (blank? %) "/" %)))) 22 | 23 | (defn read-key [^String key-str] 24 | (-> key-str 25 | .getBytes 26 | ByteArrayInputStream. 27 | pem/read)) 28 | 29 | (defn get-hex-encoded-digested-string [msg] 30 | (msg->sha512 msg)) 31 | 32 | (defn str->bytes 33 | "Convert string to byte array." 34 | ([^String s] 35 | (str->bytes s "UTF-8")) 36 | ([^String s, ^String encoding] 37 | (.getBytes s encoding))) 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | dist: noble 3 | 4 | addons: 5 | apt: 6 | packages: 7 | - gzip 8 | 9 | 10 | jdk: openjdk17 11 | 12 | rvm: 13 | - 2.6 14 | 15 | stages: 16 | - name: test 17 | if: branch = develop 18 | - name: publish 19 | if: tag IS present 20 | 21 | cache: bundler 22 | 23 | jobs: 24 | include: 25 | - stage: build 26 | name: "Build & Test" 27 | script: 28 | - lein test 29 | - stage: test 30 | install: 31 | - lein deps 32 | - lein pom 33 | script: 34 | - lein test 35 | - stage: publish 36 | install: 37 | - lein deps 38 | - lein pom 39 | script: lein test 40 | deploy: 41 | provider: script 42 | script: lein deploy 43 | cleanup: false 44 | on: 45 | tags: true 46 | all_branches: true 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Medidata Solutions Worldwide 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/clojure_mauth_client/credentials.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.credentials 2 | (:require [clojure.string :refer [blank?]])) 3 | 4 | (defonce ^:private credential (atom {})) 5 | 6 | (defn define-credentials [app-uuid private-key mauth-service-url] 7 | (let [cred {:app-uuid app-uuid 8 | :private-key private-key 9 | :mauth-service-url mauth-service-url}] 10 | (if (or (blank? app-uuid) 11 | (blank? private-key) 12 | (blank? mauth-service-url)) 13 | (throw (Exception. "Please be sure the app-uuid, private-key and mauth-service-url are defined and valid, 14 | either in the environment variables APP_UUID, MAUTH_KEY and MAUTH_SERVICE_URL respectively. 15 | Or, you may call define-credentials to set them manually.")) 16 | (reset! credential cred)))) 17 | 18 | (defn get-credentials [] 19 | (if (empty? @credential) 20 | (define-credentials (System/getenv "APP_UUID") 21 | (System/getenv "MAUTH_KEY") 22 | (System/getenv "MAUTH_SERVICE_URL"))) 23 | @credential) -------------------------------------------------------------------------------- /src/clojure_mauth_client/header.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.header 2 | (:require 3 | [clojure-mauth-client.util :as util] 4 | [clojure.data.codec.base64 :as base64] 5 | [clojure.string :as str] 6 | [pem-reader.core :as pem]) 7 | (:use digest) 8 | (:import 9 | (javax.crypto Cipher))) 10 | 11 | (defn- sign-mauth [message app-uuid private-key] 12 | (->> (let [private-key (pem/private-key (util/read-key private-key)) 13 | cipher (doto (Cipher/getInstance "RSA/ECB/PKCS1Padding") 14 | (.init Cipher/ENCRYPT_MODE private-key))] 15 | (.doFinal cipher (.getBytes message))) 16 | base64/encode 17 | String. 18 | (str "MWS " app-uuid ":"))) 19 | 20 | (defn- make-mws-auth-string [verb url body app-uuid time] 21 | (->> [verb (util/get-uri url) body app-uuid time] 22 | (str/join "\n"))) 23 | 24 | (defn- make-mws-auth-string-for-response [status body app-uuid time] 25 | (->> [status body app-uuid time] 26 | (str/join "\n"))) 27 | 28 | (defn build-mauth-headers 29 | ([verb url body app-uuid private-key] 30 | (let [x-mws-time (util/epoch-seconds) 31 | x-mws-authentication (make-mws-auth-string verb url body app-uuid x-mws-time)] 32 | {"X-MWS-Authentication" (-> x-mws-authentication 33 | util/msg->sha512 34 | (sign-mauth app-uuid private-key)) 35 | "X-MWS-Time" (str x-mws-time)})) 36 | ([status body app-uuid private-key] 37 | (let [x-mws-time (util/epoch-seconds) 38 | x-mws-authentication (make-mws-auth-string-for-response status body app-uuid x-mws-time)] 39 | {"X-MWS-Authentication" (-> x-mws-authentication 40 | util/msg->sha512 41 | (sign-mauth app-uuid private-key)) 42 | "X-MWS-Time" (str x-mws-time)}))) 43 | 44 | -------------------------------------------------------------------------------- /src/clojure_mauth_client/header_v2.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.header-v2 2 | (:require [pem-reader.core :as pem] 3 | [clojure.data.codec.base64 :as base64] 4 | [jdk.security.Signature :as signature] 5 | [clojure-mauth-client.util :as util])) 6 | 7 | (defn- encrypt-signature-rsa [private-key-string string-to-sign] 8 | (let [signature-instance (signature/*get-instance "SHA512withRSA") 9 | private-key (pem/private-key (util/read-key private-key-string)) 10 | byte-array-to-sign (util/str->bytes (String. string-to-sign))] 11 | (signature/init-sign signature-instance private-key) 12 | (signature/update signature-instance byte-array-to-sign 0 (alength byte-array-to-sign)) 13 | (->> (signature/sign signature-instance) 14 | base64/encode 15 | String. 16 | (str "")))) 17 | 18 | (defn- generate-headers-v2 [mcc-auth-string-to-sign app-uuid private-key] 19 | (let [encrypted-signature (encrypt-signature-rsa private-key mcc-auth-string-to-sign) 20 | mcc-authentication (str "MWSV2" " " app-uuid ":" encrypted-signature ";")] 21 | mcc-authentication)) 22 | 23 | (defn build-mauth-headers-v2 24 | ([verb-or-status body app-uuid private-key] 25 | (build-mauth-headers-v2 verb-or-status nil body app-uuid private-key nil)) 26 | ([verb-or-status url body app-uuid private-key query-param] 27 | (let [mcc-time (util/epoch-seconds) 28 | params-vec (if (instance? java.lang.String verb-or-status) 29 | [verb-or-status (util/get-uri url) (util/get-hex-encoded-digested-string body) app-uuid mcc-time query-param] 30 | [verb-or-status (util/get-hex-encoded-digested-string body) app-uuid mcc-time]) 31 | all-params-string (clojure.string/join "\n" params-vec) 32 | authentication (generate-headers-v2 all-params-string app-uuid private-key)] 33 | {"mcc-authentication" authentication 34 | "mcc-time" (str mcc-time)}))) -------------------------------------------------------------------------------- /src/clojure_mauth_client/validate.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.validate 2 | (:require [clojure.data.json :as json] 3 | [clojure.string :refer [trim]]) 4 | (:use clojure-mauth-client.request 5 | clojure-mauth-client.credentials 6 | clojure.data.codec.base64)) 7 | 8 | (def ^{:private true} auth-uri "/mauth/v1/authentication_tickets.json") 9 | 10 | (defn- build-auth-ticket-body 11 | [verb app-uuid request-url body time signature] 12 | {:authentication_ticket 13 | {:app_uuid app-uuid 14 | :verb verb 15 | :request_url request-url 16 | :b64encoded_body (String. (encode (.getBytes body))) 17 | :request_time time 18 | :client_signature signature}}) 19 | 20 | (defn signature-map [msg] 21 | (let [signature msg 22 | values-fn (fn [s] (rest (re-find #"\A(\S+)\s*([^:]+):([^:]+)\z" s)))] 23 | (if (nil? signature) {} 24 | (zipmap [:token :app-uuid :signature] (values-fn (trim signature)))))) 25 | 26 | (defn validate! 27 | ([verb uri body time signature] 28 | (validate! verb uri body time signature nil)) 29 | ([verb uri body time signature mauth-version] 30 | (validate! verb uri body time signature mauth-version nil)) 31 | ([verb uri body time signature mauth-version query-string] 32 | (let [creds (get-credentials) 33 | sig (signature-map signature) 34 | auth-body (build-auth-ticket-body verb (:app-uuid sig) uri body time (:signature sig)) 35 | updated-body (cond-> auth-body 36 | (= mauth-version "v2") (assoc-in [:authentication_ticket :token] (:token sig)) 37 | (and (= mauth-version "v2") query-string) (assoc-in [:authentication_ticket :query_string] query-string)) 38 | auth-ticket-body (json/write-str updated-body)] 39 | (-> 40 | (post! (:mauth-service-url creds) 41 | auth-uri 42 | auth-ticket-body 43 | :additional-headers {"Content-Type" "application/json"}) 44 | :status 45 | (= 204))))) 46 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure-mauth-client "2.0.9-SNAPSHOT" 2 | :description "Clojure Mauth Client" 3 | :url "https://github.com/mdsol/clojure-mauth-client" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.12.0"] 7 | [xsc/pem-reader "0.1.1"] 8 | [digest "1.4.10"] 9 | [org.clojure/data.codec "0.1.1"] 10 | [clojure-interop/java.security "1.0.5"] 11 | [http-kit "2.4.0-alpha2"] 12 | [clj-http "3.13.0"] 13 | [org.clojure/data.json "2.5.0"] 14 | [javax.xml.bind/jaxb-api "2.3.1"]] 15 | 16 | :deploy-repositories [["releases" 17 | {:url "https://clojars.org/repo" 18 | :sign-releases false 19 | :username :env/CLOJARS_USERNAME 20 | :password :env/CLOJARS_DEPLOY_TOKEN}]] 21 | 22 | :release-tasks [["vcs" "assert-committed"] 23 | ["change" "version" "leiningen.release/bump-version" "release"] 24 | ["vcs" "commit"] 25 | ["vcs" "tag" "--no-sign"] 26 | ["vcs" "push"]] 27 | 28 | :aliases {"bump!" ^{:doc "Bump the project version number and push the commits to the original repository."} 29 | ["do" 30 | ["vcs" "assert-committed"] 31 | ["change" "version" "leiningen.release/bump-version"] 32 | ["vcs" "commit"] 33 | ["vcs" "push"]]} 34 | 35 | :target-path "target/%s" 36 | :profiles {:uberjar {:aot :all 37 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}} 38 | 39 | :jvm-opts ~(concat 40 | [] ;other opts... 41 | (if (let [v (-> (System/getProperty "java.version") 42 | (clojure.string/split #"[.]") 43 | first 44 | Integer.)] 45 | (and (>= v 9) (< v 11))) 46 | ["--add-modules" "java.xml.bind"] 47 | [])) 48 | :aot :all) 49 | -------------------------------------------------------------------------------- /src/clojure_mauth_client/middleware.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.middleware 2 | (:require [clojure.string :refer [lower-case]] 3 | [clojure.data.json :as json]) 4 | (:use clojure-mauth-client.validate)) 5 | 6 | (defn- downcase-header-keys [headers] 7 | (reduce-kv 8 | (fn [m k v] 9 | (assoc m (lower-case (name k)) v)) 10 | {} headers)) 11 | 12 | (defn wrap-mauth-verification [handler] 13 | (fn [request] 14 | (let [{method :request-method 15 | uri :uri 16 | body :body 17 | query-string :query-string} request 18 | serialized-body (cond (nil? body) "" 19 | (string? body) body 20 | (map? body) (json/write-str body) 21 | :else (slurp body)) 22 | headers (-> (:headers request) 23 | downcase-header-keys) 24 | [mauth-time mauth-auth] (cond 25 | (every? headers 26 | ["mcc-time" 27 | "mcc-authentication"]) [(get headers "mcc-time") 28 | (get headers "mcc-authentication")] 29 | (every? headers 30 | ["x-mws-time" 31 | "x-mws-authentication"]) [(get headers "x-mws-time") 32 | (get headers "x-mws-authentication")]) 33 | mauth-version (let [signature (signature-map mauth-auth) 34 | token (:token signature)] 35 | (cond 36 | (= token "MWSV2") "v2" 37 | (= token "MWS") "v1")) 38 | valid? (validate! (.toUpperCase (name method)) uri serialized-body mauth-time mauth-auth mauth-version query-string)] 39 | (if valid? 40 | (handler (-> request 41 | (assoc :body serialized-body))) 42 | {:status 401 43 | :body "Unauthorized."})))) 44 | -------------------------------------------------------------------------------- /test/clojure_mauth_client/validate_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.validate-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [clojure-mauth-client.validate :as mauth-validate :refer [validate!]] 4 | [clojure-mauth-client.request :refer [post!]] 5 | [clojure-mauth-client.credentials :refer [get-credentials]] 6 | [clojure.data.json :as json])) 7 | 8 | (def ^:private request-data 9 | {:verb (.toUpperCase (name :post)) 10 | :url "https://www.mdsol.com/api/v1/testing" 11 | :body "\"{\"test\":{\"request\":123}}\"" 12 | :time "1532825948" 13 | :v1-signature "MWS a5a733c5-2bae-400c-aae9-6bb5b99d4130:SpjzqMFJ0cl8Lvi72TcU1qfVP9rzRWH/Jys2g==;" 14 | :v2-signature "MWSV2 a5a733c5-2bae-400c-aae9-6bb5b99d4130:T0XZu8X6bUcKBW/QgX0RnUg0hfbcDfm==;"}) 15 | 16 | (deftest test-validate-success 17 | (with-redefs [post! (fn [& args] 18 | {:status 204}) 19 | get-credentials (constantly {:mauth-service-url "http://test.com"})] 20 | 21 | (testing "testing validate with mauth v1 version" 22 | (let [{:keys [verb url body time v1-signature]} request-data] 23 | (is (true? (validate! verb url body time v1-signature))))) 24 | 25 | (testing "testing validate with mauth v2 version" 26 | (let [{:keys [verb url body time v2-signature]} request-data] 27 | (is (true? (validate! verb url body time v2-signature "v2"))))))) 28 | 29 | (deftest test-validate-failure 30 | (with-redefs [post! (fn [& args] 31 | {:status 400}) 32 | get-credentials (constantly {:mauth-service-url "http://test.com"})] 33 | 34 | (testing "testing validate with mauth v1 version" 35 | (let [{:keys [verb url body time v1-signature]} request-data] 36 | (is (false? (validate! verb url body time v1-signature))))) 37 | 38 | (testing "testing validate with mauth v2 version" 39 | (let [{:keys [verb url body time v2-signature]} request-data] 40 | (is (false? (validate! verb url body time v2-signature "v2"))))))) 41 | 42 | (deftest test-auth-ticket-body 43 | (let [auth-ticket-atom (atom nil)] 44 | (with-redefs [post! (fn [_ _ auth-ticket-body & _args] 45 | (reset! auth-ticket-atom auth-ticket-body)) 46 | get-credentials (constantly {:mauth-service-url "http://test.com"})] 47 | (testing "V1 protocol" 48 | (let [{:keys [verb url body time v1-signature]} request-data 49 | _ (validate! verb url body time v1-signature) 50 | auth-ticket (json/read-str @auth-ticket-atom :key-fn keyword)] 51 | (is (= verb (get-in auth-ticket [:authentication_ticket :verb]))) 52 | (is (= url (get-in auth-ticket [:authentication_ticket :request_url]))) 53 | (is (nil? (get-in auth-ticket [:authentication_ticket :token]))) 54 | (is (nil? (get-in auth-ticket [:authentication_ticket :query_string]))))) 55 | (testing "V2 protocol" 56 | (let [{:keys [verb url body time v2-signature]} request-data 57 | query-string "color=black&model=mustang" 58 | _ (validate! verb url body time v2-signature "v2" query-string) 59 | auth-ticket (json/read-str @auth-ticket-atom :key-fn keyword)] 60 | (is (= verb (get-in auth-ticket [:authentication_ticket :verb]))) 61 | (is (= url (get-in auth-ticket [:authentication_ticket :request_url]))) 62 | (is (= "MWSV2" (get-in auth-ticket [:authentication_ticket :token]))) 63 | (is (= query-string (get-in auth-ticket [:authentication_ticket :query_string])))))))) 64 | -------------------------------------------------------------------------------- /src/clojure_mauth_client/request.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.request 2 | (:require [org.httpkit.client :as http] 3 | [clj-http.client :as client] 4 | [clojure.data.json :as json] 5 | [clojure-mauth-client.header :as header] 6 | [clojure-mauth-client.header-v2 :as header-v2] 7 | [clojure-mauth-client.credentials :as credentials]) 8 | (:import (javax.net.ssl SSLEngine SNIHostName SSLParameters) 9 | (java.net URI))) 10 | 11 | (defn- sni-configure 12 | [^SSLEngine ssl-engine ^URI uri] 13 | (let [^SSLParameters ssl-params (.getSSLParameters ssl-engine)] 14 | (.setServerNames ssl-params [(SNIHostName. (.getHost uri))]) 15 | (.setSSLParameters ssl-engine ssl-params))) 16 | 17 | (defn build-header [mauth-version query-string params] 18 | (let [params-with-query-string (conj params query-string)] 19 | (if (or (not (seq mauth-version)) (not (^String .equalsIgnoreCase mauth-version "v2"))) 20 | (apply header/build-mauth-headers params) 21 | (apply header-v2/build-mauth-headers-v2 params-with-query-string)))) 22 | 23 | (defn make-request [type base-url uri body & {:keys [additional-headers with-sni? throw-exceptions?] 24 | :or {additional-headers {} 25 | with-sni? nil 26 | throw-exceptions? false}}] 27 | (let [cred (credentials/get-credentials) 28 | mauth-version (additional-headers :mauth-version) 29 | query-params (additional-headers :query-param-string) 30 | ; Tech debt: test with-sni?=true and modify this code if needed 31 | ; (https://jira.mdsol.com/browse/MCC-767309) 32 | options (if with-sni? {:client (http/make-client {:ssl-configurer sni-configure})} {}) 33 | response (-> [(.toUpperCase type) (str base-url uri) (str body) (:app-uuid cred) (:private-key cred)] 34 | (#(build-header mauth-version query-params %)) 35 | (merge additional-headers) 36 | ; We use clj-http instead of http-kit because it is supported by the motel-java tracing agent 37 | (#(client/request (-> {:headers % 38 | :url (str base-url uri) 39 | :method (keyword (.toLowerCase type)) 40 | :body body 41 | :throw-exceptions throw-exceptions? 42 | :as :auto} 43 | (merge options)))))] 44 | ; The following line is here because existing clients expect a String instead of a LazySeq. 45 | ; When we're ready to make a breaking change, we should return "response" directly with no modification. 46 | (update response :body json/write-str))) 47 | 48 | (defn get! [base-url uri & {:keys [additional-headers with-sni?] 49 | :or {additional-headers {} 50 | with-sni? nil}}] 51 | (make-request "GET" base-url uri "" :additional-headers additional-headers 52 | :with-sni? with-sni?)) 53 | 54 | (defn post! [base-url uri body & {:keys [additional-headers with-sni?] 55 | :or {additional-headers {} 56 | with-sni? nil}}] 57 | (make-request "POST" base-url uri body :additional-headers additional-headers 58 | :with-sni? with-sni?)) 59 | 60 | (defn delete! [base-url uri & {:keys [additional-headers with-sni?] 61 | :or {additional-headers {} 62 | with-sni? nil}}] 63 | (make-request "DELETE" base-url uri "" :additional-headers additional-headers 64 | :with-sni? with-sni?)) 65 | 66 | (defn put! [base-url uri body & {:keys [additional-headers with-sni?] 67 | :or {additional-headers {} 68 | with-sni? nil}}] 69 | (make-request "PUT" base-url uri body :additional-headers additional-headers 70 | :with-sni? with-sni?)) -------------------------------------------------------------------------------- /test/clojure_mauth_client/header_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.header-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mauth-client.header :as header] 4 | [clojure-mauth-client.util :as util] 5 | [clojure-mauth-client.credentials :as credentials])) 6 | 7 | (use-fixtures 8 | :once 9 | (fn [f] 10 | ;Note: these are NOT valid real credentials. 11 | (credentials/define-credentials "abcd7d78-c874-47d9-a829-ccaa51ae75c9" 12 | "-----BEGIN RSA PRIVATE KEY----- 13 | MIIEowIBAAKCAQEAsaa4gcNl4jx9YF7Y/6B+v29c0KBs1vxym0p4hwjZl5GteQgR 14 | uFW5wM93F2lUFiVEQoM+Ti3AQjEDWdeuOIfo66LgbNLH7B3JhbkwHti/bMsq7T66 15 | Gs3cOhMcKDrTswOv8x72QzsOf1FNs7Yzsu1iwJpttNg+VCRj169hQ/YI39KSuYzQ 16 | aXdjK0++EPsFtr2fU7E4cHGCDlAr8Tgt2gY8xmIuLF513jqJ2fhurja+YZriGwuO 17 | qdBDLVpJOB1iz8bQ1CGMMGbkYl64jfsMHMBeueP9RyZ50I2Jrr8v05qG2dYDeQYn 18 | d7BjAy2VLiWewRFQRltMOWC3nbZAla+282J/swIDAQABAoIBAAvPfrK5z9szlE5E 19 | 3/5WqDaH686+6517WQ8z60Fm+DhYagUC4VK0+E12PX+j9AAo6BnX6dt+tSpxYbym 20 | VyHQ/04zHOJ/POVYsZ4fSrCyTj+oXik5o1vG1d5SiOuvxYVAOIFcTJj5oyQZvqW0 21 | 9kjt+UO+wI5mVfZ4GN8s/LVs9PgURmYiSzy9Ed43kc4p9pSLQ448f7mnv134WknT 22 | l/2z5/+q0DXj2Nx/JV0OsTR5KV4b0u87aSNrRldcKnfOWK9BubP4T9yGMjPyeuZP 23 | XA0UQMRlGpql8I+4rRvdHio+N2lpHsFDzd7ckmjWMjXdeU2dGhE6580YbEpuLQRb 24 | SN86ylkCgYEA3d/0W2S4wtmbV4X9AhLDe4vWLP1GmMcmcZ2wwuljEK14xNdeYzyD 25 | z42PZ6RsGtAtdUHe6/e+7TRpz/f938f4ygawCV7zXkF65bPm54zz9o7+CM9p4P3V 26 | AntDVO5IskiE6hb5A7wR0UdRNLDhov38Ngh+iUEDJ9MuZ75GWj6UbGcCgYEAzPmE 27 | om9eaDC3g2hmBwTB54bAMgskRT9sdaFFJtMHx7VU0xw9E34qfD17o0LDa9n62TDv 28 | getytWnluzwyhRw6UPhzseQw6R+7wtLlLPAj9mC58l34ONXwPSME0Exp/9WAVErt 29 | Z0Ng6xPmifesQ/fSUM52aCIgufyQOQ1zCx9ogtUCgYBotpOKtqSEQVMRIYlg+x4L 30 | Jtnz7azt2b+JC5UqyB8a9ePzcnl3eE31HKg7j9v9Y5awql/dGdWf+Yaewjms7aG7 31 | JyDZq1hMebbYxekKCvnwuVenLMyZhPKM80O5x6PDkHo6SJFJc+8sx+3JYll7JUds 32 | 8OFXQbmNiBt0ltZ5LOO7rQKBgB1/HrYdXrGRqSbw5BXIenrt6kSJU+vfJ6V50rC2 33 | l50GnDFRE/z1H/oHAv7IgcTIdo/AugaxMi2nEpcyH3cGS+IRDt0foGY72dI8dRxV 34 | ZmdzHe8h1LGhH9Q8cNnk1TAqsi/vJGDC0nShxYA/MvwI8qwMOf/cQWdiUALVy6Nj 35 | HrANAoGBAIK6Lh0mZLtan60F1rUoXqME7yIGQgf/cP+RKLwE36kqF5DQx9aFwPZx 36 | 9aWuKH+/XjdHf/J1n/AQ1j/G/WExs3UNfrvDgYea5QDnvc2gMBDRkdBwFZHYZLIn 37 | e+viqMbgmORJDP/8vbpd0yZjT25ImysJE5cSCGiqHOotDs3jdlUX 38 | -----END RSA PRIVATE KEY-----" 39 | "https://mauth-sandbox.imedidata.net") 40 | (with-redefs [util/epoch-seconds (fn [] 1532825948)] 41 | (f)))) 42 | 43 | (deftest header-test 44 | (testing "It should generate a valid mauth X-MWS header for GET" 45 | (let [creds (credentials/get-credentials) 46 | mauth-headers (header/build-mauth-headers "GET" "https://www.mdsol.com/api/v2/testing" "" (:app-uuid creds) (:private-key creds))] 47 | (is (= {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:gI/yUeSTbiOWggLvCv2IJP19GFvmlE8RoaUrIpyLE8DY/mCQd8CUPgT9xNHGNqgPGe9f4CZdiFCC79Xvp6seZAq8/CnqA1dsJW6f46scqqTs+4N1TJml6GNCT9xU4tjUyHWFWpCBQlSvpoTFsLSq2d2zas9M9q1sgwPBS/oPGEN1agCQLHZS/Ime4ub8MuXh0Q8aWodqCpVi4GPiap/KLIQEzbvhsdayxmAcs2XDjpt+CReRf3tBCzB1RucVEfBehxtDQGgvrs/UCUbkpq7gY7f2k0RkrH+IopfhYfdNpmCHW12OEQoZ74TVbh61Uo+xcD1der46+tWk0mdnlyXKow==" 48 | "X-MWS-Time" "1532825948"} mauth-headers)))) 49 | 50 | (testing "It should generate a valid mauth X-MWS header for response" 51 | (let [creds (credentials/get-credentials) 52 | mauth-headers (header/build-mauth-headers 200 "{\"test\":\"testing\"}" (:app-uuid creds) (:private-key creds))] 53 | (is (= {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:i0rLrgEN8Jy/M4kA1PNNckUxeGU2pL3PGCOjdhRrC6egHVTvNfJLXveVG/7wAU9H4hpLYsThyV1/LRc/OupBZmKYRDzqD6OneZVLkysehk6/OHKb8j8uJQnBSLB72ooIPPIUxJqtasHCi6cdzkBEZf3omp7qzkinhuX2Wi/t70xfC5YmeTydBoe2d+zcIDJZb6+zON4V5CwGMPlCLK6iFD8+hk9ddhszQZ+siHK8SWxrhrMGRDN9xyp3ljw+f62tlpTZi0KEHAr0M/aq49aP3Iv/XrN88Cl5Tt1nGIWslarfJrAxFNfzofO0KULqFB9nRV1NkBbrSMyjGvea7nGNBw==" 54 | "X-MWS-Time" "1532825948"} mauth-headers))))) 55 | 56 | -------------------------------------------------------------------------------- /test/clojure_mauth_client/middleware_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.middleware-test 2 | (:require [clojure-mauth-client.middleware :as middleware] 3 | [clojure.test :refer [deftest testing is]] 4 | [clojure-mauth-client.validate :refer [validate!]] 5 | [clojure-mauth-client.request :refer [post!]] 6 | [clojure-mauth-client.credentials :refer [get-credentials]])) 7 | 8 | (def mock-post-request-v2 9 | {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:T0XZu8X6bUcKBW/QgX0RnUg0hfbcDfm==;" 10 | "mcc-time" "1532825948" 11 | :Content-Type "application/json" 12 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} 13 | :url "https://www.mdsol.com/api/v2/testing" 14 | :request-method :post 15 | :body "\"{\"test\":{\"request\":123}}\""}) 16 | 17 | (def mock-post-request-v1 18 | {:headers {"x-mws-authentication" "MWS a5a733c5-2bae-400c-aae9-6bb5b99d4130:SpjzqMFJ0cl8Lvi72TcU1qfVP9rzRWH/Jys2g==;" 19 | "x-mws-time" "1532825948" 20 | :Content-Type "application/json" 21 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} 22 | :url "https://www.mdsol.com/api/v1/testing" 23 | :request-method :post 24 | :body "\"{\"test\":{\"request\":123}}\""}) 25 | 26 | (def mock-post-request-without-mauth-headers 27 | {:headers {:Content-Type "application/json" 28 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} 29 | :url "https://www.mdsol.com/api/v1/testing" 30 | :request-method :post 31 | :body "\"{\"test\":{\"request\":123}}\""}) 32 | 33 | (def mock-post-request-v2-with-invalid-signature 34 | {:headers {"mcc-authentication" "MWSV abcd7d78-c874-47d9-a829-ccaa51ae75c9:T0XZu8X6bUcKBW/QgX0RnUg0hfbcDfm==;" 35 | "mcc-time" "1532825948" 36 | :Content-Type "application/json" 37 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq"} 38 | :url "https://www.mdsol.com/api/v2/testing" 39 | :request-method :post 40 | :body "\"{\"test\":{\"request\":123}}\""}) 41 | 42 | (defn mock-handler [{:keys [body] :as request}] 43 | {:body body 44 | :status 200}) 45 | 46 | (deftest test-wrap-mauth-verification 47 | (testing "Request should get validated successfully" 48 | (with-redefs [validate! (fn [& _] (constantly true))] 49 | (let [request-function (middleware/wrap-mauth-verification mock-handler) 50 | {:keys [status body]} (request-function mock-post-request-v2)] 51 | (is (= 200 status)) 52 | (is (= "\"{\"test\":{\"request\":123}}\"" body))))) 53 | 54 | (testing "Request should get invalidated and should return Unauthorized with 401 error code with v2" 55 | (with-redefs [validate! (fn [& _] false)] 56 | (let [request-function (middleware/wrap-mauth-verification mock-handler) 57 | {:keys [status body]} (request-function mock-post-request-v2)] 58 | (is (= 401 status)) 59 | (is (= "Unauthorized." body))))) 60 | 61 | (testing "Request should get invalidated and should return Unauthorized with 401 error code with v1" 62 | (with-redefs [validate! (fn [& _] false)] 63 | (let [request-function (middleware/wrap-mauth-verification mock-handler) 64 | {:keys [status body]} (request-function mock-post-request-v1)] 65 | (is (= 401 status)) 66 | (is (= "Unauthorized." body))))) 67 | 68 | (testing "Request should get validated as per v2 version" 69 | (with-redefs [post! (fn [& args] 70 | {:status 204}) 71 | get-credentials (constantly {:mauth-service-url "http://test.com"})] 72 | (let [request-function (middleware/wrap-mauth-verification mock-handler) 73 | {:keys [status body]} (request-function mock-post-request-v2)] 74 | (is (= 200 status))))) 75 | 76 | (testing "Request should get validated as per v1 version" 77 | (with-redefs [post! (fn [& args] 78 | {:status 204}) 79 | get-credentials (constantly {:mauth-service-url "http://test.com"})] 80 | (let [request-function (middleware/wrap-mauth-verification mock-handler) 81 | {:keys [status body]} (request-function mock-post-request-v1)] 82 | (is (= 200 status)))))) 83 | -------------------------------------------------------------------------------- /test/clojure_mauth_client/header_v2_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.header-v2-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mauth-client.header-v2 :as header-v2] 4 | [clojure-mauth-client.credentials :as credentials] 5 | [clojure-mauth-client.util :as util])) 6 | 7 | (use-fixtures 8 | :once 9 | (fn [f] 10 | ;Note: these are NOT valid real credentials. 11 | (credentials/define-credentials "abcd7d78-c874-47d9-a829-ccaa51ae75c9" 12 | "-----BEGIN RSA PRIVATE KEY----- 13 | MIIEowIBAAKCAQEAsaa4gcNl4jx9YF7Y/6B+v29c0KBs1vxym0p4hwjZl5GteQgR 14 | uFW5wM93F2lUFiVEQoM+Ti3AQjEDWdeuOIfo66LgbNLH7B3JhbkwHti/bMsq7T66 15 | Gs3cOhMcKDrTswOv8x72QzsOf1FNs7Yzsu1iwJpttNg+VCRj169hQ/YI39KSuYzQ 16 | aXdjK0++EPsFtr2fU7E4cHGCDlAr8Tgt2gY8xmIuLF513jqJ2fhurja+YZriGwuO 17 | qdBDLVpJOB1iz8bQ1CGMMGbkYl64jfsMHMBeueP9RyZ50I2Jrr8v05qG2dYDeQYn 18 | d7BjAy2VLiWewRFQRltMOWC3nbZAla+282J/swIDAQABAoIBAAvPfrK5z9szlE5E 19 | 3/5WqDaH686+6517WQ8z60Fm+DhYagUC4VK0+E12PX+j9AAo6BnX6dt+tSpxYbym 20 | VyHQ/04zHOJ/POVYsZ4fSrCyTj+oXik5o1vG1d5SiOuvxYVAOIFcTJj5oyQZvqW0 21 | 9kjt+UO+wI5mVfZ4GN8s/LVs9PgURmYiSzy9Ed43kc4p9pSLQ448f7mnv134WknT 22 | l/2z5/+q0DXj2Nx/JV0OsTR5KV4b0u87aSNrRldcKnfOWK9BubP4T9yGMjPyeuZP 23 | XA0UQMRlGpql8I+4rRvdHio+N2lpHsFDzd7ckmjWMjXdeU2dGhE6580YbEpuLQRb 24 | SN86ylkCgYEA3d/0W2S4wtmbV4X9AhLDe4vWLP1GmMcmcZ2wwuljEK14xNdeYzyD 25 | z42PZ6RsGtAtdUHe6/e+7TRpz/f938f4ygawCV7zXkF65bPm54zz9o7+CM9p4P3V 26 | AntDVO5IskiE6hb5A7wR0UdRNLDhov38Ngh+iUEDJ9MuZ75GWj6UbGcCgYEAzPmE 27 | om9eaDC3g2hmBwTB54bAMgskRT9sdaFFJtMHx7VU0xw9E34qfD17o0LDa9n62TDv 28 | getytWnluzwyhRw6UPhzseQw6R+7wtLlLPAj9mC58l34ONXwPSME0Exp/9WAVErt 29 | Z0Ng6xPmifesQ/fSUM52aCIgufyQOQ1zCx9ogtUCgYBotpOKtqSEQVMRIYlg+x4L 30 | Jtnz7azt2b+JC5UqyB8a9ePzcnl3eE31HKg7j9v9Y5awql/dGdWf+Yaewjms7aG7 31 | JyDZq1hMebbYxekKCvnwuVenLMyZhPKM80O5x6PDkHo6SJFJc+8sx+3JYll7JUds 32 | 8OFXQbmNiBt0ltZ5LOO7rQKBgB1/HrYdXrGRqSbw5BXIenrt6kSJU+vfJ6V50rC2 33 | l50GnDFRE/z1H/oHAv7IgcTIdo/AugaxMi2nEpcyH3cGS+IRDt0foGY72dI8dRxV 34 | ZmdzHe8h1LGhH9Q8cNnk1TAqsi/vJGDC0nShxYA/MvwI8qwMOf/cQWdiUALVy6Nj 35 | HrANAoGBAIK6Lh0mZLtan60F1rUoXqME7yIGQgf/cP+RKLwE36kqF5DQx9aFwPZx 36 | 9aWuKH+/XjdHf/J1n/AQ1j/G/WExs3UNfrvDgYea5QDnvc2gMBDRkdBwFZHYZLIn 37 | e+viqMbgmORJDP/8vbpd0yZjT25ImysJE5cSCGiqHOotDs3jdlUX 38 | -----END RSA PRIVATE KEY-----" 39 | "https://mauth-sandbox.imedidata.net") 40 | (with-redefs [util/epoch-seconds (fn [] 1532825948)] 41 | (f)))) 42 | 43 | (deftest header-v2-test 44 | (testing "It should generate a valid mauth MWSV2 header for POST" 45 | (let [creds (credentials/get-credentials) 46 | 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")] 47 | (is (= {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:nB0sLA13YT3n5HYeuox4iiGTPNrxS0XsgWRNIC51U3Pd0FFTBlbHVO+5URTywg5BAmC3hSruFP6S116ewjJE2lUoNdXSp9gsRt6GZ6Y/QGrd+U54TWKuk13rumiphljb4HdslAOe17ac6nqRxqF/Rd16lK/0nvTpPN4Z1USJ0L0YwJDUpkhaaTsvt6LIeIZjY6wexikZK/q4CvXQDwRmhtdaTICdAMjvbhJ6apnR3e8Ra9I9wmb713DudcBn2uerHuURMywTkqUCZMcH4cF3a8Hi6Lpm85BfzP+9RRzcUJbU5hkzIor58hrsvJv/9WVFGrt8JExnmcWKrgA/rj2X2g==;" 48 | "mcc-time" "1532825948"} mauth-header-v2)))) 49 | 50 | (testing "It should generate a valid mauth MWSV2 header for response" 51 | (let [creds (credentials/get-credentials) 52 | mauth-headers-v2 (header-v2/build-mauth-headers-v2 200 "{\"test\":\"testing\"}" (:app-uuid creds) (:private-key creds))] 53 | (is (= {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:pTk0Kfs6utta/wixDIwmycbPR5MMPKkU5ETZ+SlUlAMIx2o3sJxMc10ezfdO+z34l47yPIvyFytE+StMkJiLQMDpA9eKl2Xek3QdYL5YBZlG6j1b2lVgF0ptWqxXvQXlv7ets9K6BZ/HGEYj3/3IXzeNRNKYrDCTtGv8JSjdqqpLxSFMlpB++MkjXNlTh2AwIQua5ZTTSpcw8lgxA9PgXQPDeT88bqx2jhsfELG3IG3Sn9DVPOzprsicpVljYHzqjb1+wtNCaD3PYBrkDQZeVd9Y9jU2Hy6CdjnScFQvZlqg043GKQY4OJGvxVUsJDkQHkb7jzLjXfypcvvXHulWDA==;" 54 | "mcc-time" "1532825948"} mauth-headers-v2))))) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clojure MAuth Client 2 | 3 | [![Build Status](https://travis-ci.com/mdsol/clojure-mauth-client.svg?token=16x4FpKmXowpz3fnJFpj&branch=master)](https://travis-ci.com/mdsol/clojure-mauth-client) 4 | 5 | Leiningen/Boot Coords: 6 | 7 | ```[clojure-mauth-client "2.0.8"]``` 8 | 9 | Here it is folks, a nice, clean MAuth client done in the simplest and most minimal way possible, for your Clojure application. 10 | 11 | There are a minimal set of dependencies (Bouncy Castle isn't one of them!), it is fast, and can even be used as middleware for securing your applications. 12 | 13 | You do **NOT** need to jump through hoops to use this library. Simple is as simple does. 14 | 15 | ## Building 16 | 17 | Either use the built jar, or `lein jar` to build one based on source. 18 | 19 | ## Usage 20 | 21 | First, you need to give the library your `App UUID`, `MAuth Private Key` and `MAuth Service URL`. 22 | 23 | You can do this by defining the environment variables `APP_UUID`, `MAUTH_KEY` and `MAUTH_SERVICE_URL` respectively. 24 | 25 | Or, you may do this programmatically by calling `credentials/define-credentials` with the respective parameters. 26 | 27 | You can make an MAuth call by requiring the `request` namespace and either doing `get!`, `post!`, `delete!` or `put!` with the necessary parameters. 28 | These are the most common verbs in use out there, but its easy to extend support to other verbs as needed (PRs welcome!). 29 | If the verb you want isn't in the namespace, you can also make a call to `request/make-request` and specify your action directly. 30 | It is possible to do a macro for this stuff, but I am not a big fan of that approach for this case, because you lose IDE support, and it will appear as undefined there, which is annoying. 31 | 32 | 33 | Full example (standard usage): 34 | 35 | ``` 36 | (ns your-app.core 37 | (:require [clojure-mauth-client.request :as mauth] 38 | [clojure.data.json :as json])) 39 | 40 | ;either your app has to have the APP_UUID, MAUTH_KEY, and MAUTH_SERVICE_URL predefined in env... 41 | ;or... you can call define-credentials... 42 | 43 | ;do the call! 44 | (-> (mauth/get! "https://www.mdsol.com" "/api/v2/testing.json") 45 | :body 46 | json/read-str) 47 | ``` 48 | 49 | Middleware Usage: 50 | 51 | ``` 52 | (ns your-app.handler 53 | (:require [clojure-mauth-client.middleware :refer [wrap-mauth-verification]])) 54 | ... 55 | ... 56 | (-> #'app-routes 57 | (wrap-routes middleware/wrap-formats) 58 | (wrap-mauth-verification)) 59 | ... 60 | ... 61 | ``` 62 | 63 | Yep, it really is that simple! 64 | 65 | Recently, it has come to my attention that cloudflare or cloudfront URLs (ie. api-gateway) were not supported due to SNI configuration in the load balancer. Welp, not anymore! 66 | To do your calls with SNI, pass `:with-sni? true` to your calls. 67 | 68 | For example, 69 | `(mauth/get! "https://www.mdsol.com" "/api/v2/testing.json" :with-sni? true)` 70 | 71 | Version 2.0.2 update - 72 | We have added support for mauth version 2 headers as well now. To get the v2 headers you need to add additional header in your request as 73 | "mauth-version" with value as "v2" and algorithm also uses query param if any to sign or to generate these headers, so you will need to add additional header in your request as 74 | "query-param-string" and value should be string of sorted and encoded query params e.g abc=abctest&test=testing%20value 75 | you will get the response version 2 headers as below- 76 | "mcc-authentication" authentication value 77 | "mcc-time" mcc-time 78 | 79 | Version 2.0.3 update - 80 | Changed mauth unauthorized error status code from 403 to 401 code which corresponds to the right HTTP status code for Unauthorized. 81 | And also changed signature algorithm to SHA512withRSA, previously it was SHA256withRSA. 82 | 83 | Version 2.0.4 update - 84 | With this version we have added support to validate mauth requests with v2 version and validating mcc-time and mcc-authentication headers. 85 | 86 | Version 2.0.5 update - 87 | With this version we have removed dependency of extra header "mauth-version" that we needed to validate mauth requests with v2 version. 88 | No need to send this extra header to validate v2 version requests. 89 | 90 | Version 2.0.6 update - 91 | With this version we have removed throwing exceptions when not valid mauth headers keys are added or no valid token passed in signature. 92 | 93 | Version 2.0.7 update- 94 | Updated few of the library versions to remove vulnerability 95 | 96 | Version 2.0.8 upsdate - 97 | For mAuth Protocol V2, includes the query-string (when present), in the authentication ticket to validate the requests. 98 | 99 | ## Contributing/ Tests 100 | Tests can be run using `lein test`. 101 | 102 | Contributions are welcome by Pull Request, and we welcome and encourage that! 103 | 104 | ## Release 105 | Currently, our releases are done manually, but the deploy is done by Travis/CI when a tag is present. 106 | 107 | ### How to release 108 | We use the built-in lein task `release`. This task will verify that there is nothing uncommitted, updates the release version, 109 | tags the release, deploys to clojars (Travis/CI) and then bumps the version to the next snapshot version (`lein bump!`). See `lein help release` for more details. 110 | 111 | 1. Switch to the `master` branch 112 | 2. Make sure you pull the latest code from Github 113 | 3. Create a new branch. This is because main is blocked from pushing commits directly 114 | 4. Set the upstream from this new branch `git push --set-upstream origin ` 115 | 5. run `lein release` 116 | 6. PR the changes created during the release 117 | 7. Travis CI will trigger and deploy 118 | 8. If Travis CI deploys without issues, don't merge yet, run `lein bump!` 119 | 9. PR the changes created during the `bump!` and once approved, merge the PR. 120 | 10. Verify if the new version was deployed to Clojars. 121 | 11. Pat yourself on the back! You just released the latest version of the lib 122 | 123 | ## License 124 | 125 | Copyright © 2018 Medidata Solutions 126 | 127 | This Library is licensed under the MIT licensing terms. See [LICENSE.md](https://github.com/mdsol/clojure-mauth-client/blob/master/LICENSE.md) for details 128 | -------------------------------------------------------------------------------- /test/clojure_mauth_client/request_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-mauth-client.request-test 2 | (:require [clojure.test :refer :all] 3 | [clojure-mauth-client.request :refer :all] 4 | [clojure.data.json :as json] 5 | [clojure-mauth-client.util :as util]) 6 | (:use [clojure-mauth-client.credentials])) 7 | 8 | (use-fixtures 9 | :once 10 | (fn [f] 11 | ;Note: these are NOT valid real credentials. 12 | (define-credentials "abcd7d78-c874-47d9-a829-ccaa51ae75c9" 13 | "-----BEGIN RSA PRIVATE KEY----- 14 | MIIEowIBAAKCAQEAsaa4gcNl4jx9YF7Y/6B+v29c0KBs1vxym0p4hwjZl5GteQgR 15 | uFW5wM93F2lUFiVEQoM+Ti3AQjEDWdeuOIfo66LgbNLH7B3JhbkwHti/bMsq7T66 16 | Gs3cOhMcKDrTswOv8x72QzsOf1FNs7Yzsu1iwJpttNg+VCRj169hQ/YI39KSuYzQ 17 | aXdjK0++EPsFtr2fU7E4cHGCDlAr8Tgt2gY8xmIuLF513jqJ2fhurja+YZriGwuO 18 | qdBDLVpJOB1iz8bQ1CGMMGbkYl64jfsMHMBeueP9RyZ50I2Jrr8v05qG2dYDeQYn 19 | d7BjAy2VLiWewRFQRltMOWC3nbZAla+282J/swIDAQABAoIBAAvPfrK5z9szlE5E 20 | 3/5WqDaH686+6517WQ8z60Fm+DhYagUC4VK0+E12PX+j9AAo6BnX6dt+tSpxYbym 21 | VyHQ/04zHOJ/POVYsZ4fSrCyTj+oXik5o1vG1d5SiOuvxYVAOIFcTJj5oyQZvqW0 22 | 9kjt+UO+wI5mVfZ4GN8s/LVs9PgURmYiSzy9Ed43kc4p9pSLQ448f7mnv134WknT 23 | l/2z5/+q0DXj2Nx/JV0OsTR5KV4b0u87aSNrRldcKnfOWK9BubP4T9yGMjPyeuZP 24 | XA0UQMRlGpql8I+4rRvdHio+N2lpHsFDzd7ckmjWMjXdeU2dGhE6580YbEpuLQRb 25 | SN86ylkCgYEA3d/0W2S4wtmbV4X9AhLDe4vWLP1GmMcmcZ2wwuljEK14xNdeYzyD 26 | z42PZ6RsGtAtdUHe6/e+7TRpz/f938f4ygawCV7zXkF65bPm54zz9o7+CM9p4P3V 27 | AntDVO5IskiE6hb5A7wR0UdRNLDhov38Ngh+iUEDJ9MuZ75GWj6UbGcCgYEAzPmE 28 | om9eaDC3g2hmBwTB54bAMgskRT9sdaFFJtMHx7VU0xw9E34qfD17o0LDa9n62TDv 29 | getytWnluzwyhRw6UPhzseQw6R+7wtLlLPAj9mC58l34ONXwPSME0Exp/9WAVErt 30 | Z0Ng6xPmifesQ/fSUM52aCIgufyQOQ1zCx9ogtUCgYBotpOKtqSEQVMRIYlg+x4L 31 | Jtnz7azt2b+JC5UqyB8a9ePzcnl3eE31HKg7j9v9Y5awql/dGdWf+Yaewjms7aG7 32 | JyDZq1hMebbYxekKCvnwuVenLMyZhPKM80O5x6PDkHo6SJFJc+8sx+3JYll7JUds 33 | 8OFXQbmNiBt0ltZ5LOO7rQKBgB1/HrYdXrGRqSbw5BXIenrt6kSJU+vfJ6V50rC2 34 | l50GnDFRE/z1H/oHAv7IgcTIdo/AugaxMi2nEpcyH3cGS+IRDt0foGY72dI8dRxV 35 | ZmdzHe8h1LGhH9Q8cNnk1TAqsi/vJGDC0nShxYA/MvwI8qwMOf/cQWdiUALVy6Nj 36 | HrANAoGBAIK6Lh0mZLtan60F1rUoXqME7yIGQgf/cP+RKLwE36kqF5DQx9aFwPZx 37 | 9aWuKH+/XjdHf/J1n/AQ1j/G/WExs3UNfrvDgYea5QDnvc2gMBDRkdBwFZHYZLIn 38 | e+viqMbgmORJDP/8vbpd0yZjT25ImysJE5cSCGiqHOotDs3jdlUX 39 | -----END RSA PRIVATE KEY-----" 40 | "https://mauth-sandbox.imedidata.net") 41 | 42 | (with-redefs [util/epoch-seconds (fn [] 1532825948) 43 | clj-http.client/request (fn [{:keys [client timeout filter worker-pool keepalive as follow-redirects max-redirects response 44 | trace-redirects allow-unsafe-redirect-methods proxy-host proxy-port tunnel?] 45 | :as opts 46 | :or {client nil 47 | timeout 60000 48 | follow-redirects true 49 | max-redirects 10 50 | filter nil 51 | worker-pool nil 52 | response (promise) 53 | keepalive 120000 54 | as :auto 55 | tunnel? false 56 | proxy-host nil 57 | proxy-port 3128}} 58 | & [callback]] opts)] 59 | (f)))) 60 | 61 | (def mock-get 62 | {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:gI/yUeSTbiOWggLvCv2IJP19GFvmlE8RoaUrIpyLE8DY/mCQd8CUPgT9xNHGNqgPGe9f4CZdiFCC79Xvp6seZAq8/CnqA1dsJW6f46scqqTs+4N1TJml6GNCT9xU4tjUyHWFWpCBQlSvpoTFsLSq2d2zas9M9q1sgwPBS/oPGEN1agCQLHZS/Ime4ub8MuXh0Q8aWodqCpVi4GPiap/KLIQEzbvhsdayxmAcs2XDjpt+CReRf3tBCzB1RucVEfBehxtDQGgvrs/UCUbkpq7gY7f2k0RkrH+IopfhYfdNpmCHW12OEQoZ74TVbh61Uo+xcD1der46+tWk0mdnlyXKow==" 63 | "X-MWS-Time" "1532825948"} 64 | :url "https://www.mdsol.com/api/v2/testing" 65 | :method :get, 66 | :body "\"\"", 67 | :throw-exceptions false 68 | :as :auto}) 69 | 70 | (def mock-get-with-qs 71 | {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:gI/yUeSTbiOWggLvCv2IJP19GFvmlE8RoaUrIpyLE8DY/mCQd8CUPgT9xNHGNqgPGe9f4CZdiFCC79Xvp6seZAq8/CnqA1dsJW6f46scqqTs+4N1TJml6GNCT9xU4tjUyHWFWpCBQlSvpoTFsLSq2d2zas9M9q1sgwPBS/oPGEN1agCQLHZS/Ime4ub8MuXh0Q8aWodqCpVi4GPiap/KLIQEzbvhsdayxmAcs2XDjpt+CReRf3tBCzB1RucVEfBehxtDQGgvrs/UCUbkpq7gY7f2k0RkrH+IopfhYfdNpmCHW12OEQoZ74TVbh61Uo+xcD1der46+tWk0mdnlyXKow==" 72 | "X-MWS-Time" "1532825948"} 73 | :url "https://www.mdsol.com/api/v2/testing?testABCD=1234" 74 | :method :get, 75 | :body "\"\"", 76 | :throw-exceptions false 77 | :as :auto}) 78 | 79 | (def mock-post 80 | {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:kYZR1udBwE02ct55cSWq5MZuGsC6xgVmK6TWYQ/+2IAbhqG7jZWhP95bPxTqo1f12XUWsX/oeUAp8jhvdUcsXsjVMeBwvQNgnC/HP1TNQasC2LMGfOs76WnsfKoV5zWDh+SNqMqn4pIXce3DALG9d/FB2Uu4mIg9kgQIUnfJJDljfLMjR7aMgDINPU7ToM51TqTJh+ReG7LAVwsEzSwFfj4zZFFpx8XrWn3inx5ZUvT7YcFhW4vOZaeI48HSnj80bCf1LDtWXzU7yk9gon+AlIYBTtrPQTSqyofBGZUtZCBexnoEp6NUhk5XkwqK56jizT7PZCN094kh1eofr3hSTg==" 81 | "X-MWS-Time" "1532825948"}, 82 | :url "https://www.mdsol.com/api/v2/testing1" 83 | :method :post 84 | :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" 85 | :throw-exceptions false 86 | :as :auto}) 87 | 88 | (def mock-delete 89 | {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:IzAzkGyzDtxbMlCbbWViYen/9o54B9Ijlnp1UoSIysGr/axJWwph8KRYukS+3DhYFJIBLbS1PfWI74kRTkWJl3Vmb0XgxiRfNCqresqh687ELlhNDt66p2mu/6LaVwbDKUBsIAwkQFomVfAOy3jckWZjHRySD+VABfDf4BAf5hfjTUgil63oOnH6xII51e6M160SFRz1/HpsMU/rnReniPJs22MwiqS6dhe3oU/DAzteawxujSdFA3i6Fol6kdJQN19w+0TTdOSbccjds1Wljqu/+E1ju1rXVAgcL0GuVg4dsCwrjSPY9VWfQOttpA4aHavGWNcPMh1p1kSmqlNa1g==" 90 | "X-MWS-Time" "1532825948"} 91 | :url "https://www.mdsol.com/api/v2/testing2" 92 | :method :delete 93 | :body "\"\"", 94 | :throw-exceptions false 95 | :as :auto}) 96 | 97 | (def mock-put 98 | {:headers {"X-MWS-Authentication" "MWS abcd7d78-c874-47d9-a829-ccaa51ae75c9:W+hIOQKAp0aEwDthAVaMa5ysJB8ddQdJdTWNonQoDuPEBVAY7F6GUXNoAZCYcosxgbm2rfpwyfLrS7U5b77GMFpnvvUwUSCgRziZNvfhuZfWUuW9po7OkWQUXCDvd/NtJdLOu6o1bKCGHYKjdaw/8AVH876afGyPF7+Ce2vD+YFRfY+zXF0MVWiS2WfwUwLSdOXb+Csnb21XT59zDs8qBg0gUj6WagZiJ+hYTbAt1zcNCdqs/mVt5hKA7ASxB9VY7GI4QM/n0EoyC/ruUm8DYS7kkxuxKeZuNkvpexFR4IPXQax1q7EtCIgw4yekegK210uxxYoOf+EBV2wMIVpKvw==" 99 | "X-MWS-Time" "1532825948"} 100 | :url "https://www.mdsol.com/api/v2/testing3" 101 | :method :put 102 | :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" 103 | :throw-exceptions false 104 | :as :auto}) 105 | 106 | (def mock-post-v2 107 | {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:EHrD5j5crMQ2Gv8YrEuZJKpeh0glru9ze+A2a4PoPDVyqz5DSJYmrREqRHVhIn3PrRUwFSECk+cMOH1IMOoiEe0nRg0nRWyZkbPDXfVpnQoq9cZXCmUSGCe5q8WHfqgu+gJCuXflI7n84QAFjxo7Iq+wl8oNC/RSqyqTWXPMF3dtIjiumo5091xhXyqEsUodvOtnYImvTX75pohyHjrrSdsWuVltQmQWjTzYeP4zKNmvznXVMG30BwLLuT97r5/NiyV2h9RbAMyxkGxKzcE56q8e7gddflIQGD/dgxHedcoihCzm5yHSqRFzx/X80LhCSWPlrcCBIUPGDREkUKbFwg==;" 108 | "mcc-time" "1532825948" 109 | :Content-Type "application/json" 110 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" 111 | :mauth-version "v2"} 112 | :url "https://www.mdsol.com/api/v2/testing1" 113 | :method :post 114 | :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" 115 | :throw-exceptions false 116 | :as :auto}) 117 | 118 | (def mock-put-v2 119 | {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:nBKyoJMY5qvumQhLhP0aZr2xe+058aaMuWeP8+fH3MDvxCJTectJsVPO/17wgCwP14yG/EqHK2z8/SrEjMADiOM4QtCTPgcByonCuZzcWW+zSncpssLB3ItC8K7OJ9/urZ60wOxu+0v3Nhl+jYzrVB8fIxq3HELxxIhrq1Bt41BdLNNUTBR2RG6cB2cCHB5sw2bScC1BiuwS73zOkP59Q2uRpsCfdYjWj+u9WvDV1oakUflfjaZHMqsyGMgtMMl6idkR4OejyElyQ/gv4i0dPamR4m+VogMPnlQKyGsqRyXYu1Eo8jpQR+8Mu1pa9Xyu45v18jhOJPlLpF2/1HCp2w==;" 120 | "mcc-time" "1532825948" 121 | :Content-Type "application/json" 122 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" 123 | :mauth-version "v2"} 124 | :url "https://www.mdsol.com/api/v2/testing1" 125 | :method :put 126 | :body "\"{\\\"a\\\":{\\\"b\\\":123}}\"" 127 | :throw-exceptions false 128 | :as :auto}) 129 | 130 | (def mock-get-v2 131 | {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:fdWPNlo6rTdMKxw5wf0KpQMKm22Zuf6PIUQRwIYtyzZgNEgJjgjlVdQogSYHTBVuLMx1iuyXH9m/Mex0jzD0DFZh/YIBRQ2Mn1ChKE5T0ejSyLjaTGHFg6sIpCWxVZzUvPKZKTDDdtk12vovGcRWBTfY62LpJpjlcI1YMEwcCL5A7fgIwwX9pxQO/AVJUDA9cowbBpDIjJlo9ZdcdBWfLcKfgPoWtEO5UDpvZZti0rSTiRLY3a0/l8xPLpuQXq9EjUrac0srunEK6te4XQ9MuhEQRYRkTIMF4Hvqxz9HYtkByuvpiSqNfXKCGXGxYhfAFVvRUhmk2RagISwj4qzbUA==;" 132 | "mcc-time" "1532825948" 133 | :Content-Type "application/json" 134 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" 135 | :mauth-version "v2"} 136 | :url "https://www.mdsol.com/api/v2/testing1" 137 | :method :get 138 | :body "\"\"" 139 | :throw-exceptions false 140 | :as :auto}) 141 | 142 | (def mock-get-with-qs-v2 143 | {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:fdWPNlo6rTdMKxw5wf0KpQMKm22Zuf6PIUQRwIYtyzZgNEgJjgjlVdQogSYHTBVuLMx1iuyXH9m/Mex0jzD0DFZh/YIBRQ2Mn1ChKE5T0ejSyLjaTGHFg6sIpCWxVZzUvPKZKTDDdtk12vovGcRWBTfY62LpJpjlcI1YMEwcCL5A7fgIwwX9pxQO/AVJUDA9cowbBpDIjJlo9ZdcdBWfLcKfgPoWtEO5UDpvZZti0rSTiRLY3a0/l8xPLpuQXq9EjUrac0srunEK6te4XQ9MuhEQRYRkTIMF4Hvqxz9HYtkByuvpiSqNfXKCGXGxYhfAFVvRUhmk2RagISwj4qzbUA==;" "mcc-time" "1532825948" 144 | :Content-Type "application/json" 145 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" 146 | :mauth-version "v2"} 147 | :url "https://www.mdsol.com/api/v2/testing1?testABCD=1234" 148 | :method :get 149 | :body "\"\"" 150 | :throw-exceptions false 151 | :as :auto}) 152 | 153 | (def mock-delete-v2 154 | {:headers {"mcc-authentication" "MWSV2 abcd7d78-c874-47d9-a829-ccaa51ae75c9:f2qLS57HqU7ZoJx5hlhIt9nyjbB/tqiOwcWe3Y5lO7OLL2OKuR3Et8nF5SNEu4ToWrr67nu/16ztdNRC0138uRVVEdIhnPgD3z9WZZehHaoP64BHBEcleP6MBFVJ2p/9nJvqjvZ64qAkovzQf6lGQdBSp93X8MrDN/BMSxSGOUUXffPst4nzzl09dhgCCnk0vqHG8/wDELi2I5ieCxt3WVMDj+rcffm+C0e6Oc7qT4WVXYwxnVOSOZRWo7cpd5dEXTfG0KSJK47Zdoy/qlrhSY4byk+qwky57lROMixNxdeE5PNTh6qmvvvZWRVpb+vKJraP849eacgPEyBIvAezwA==;" 155 | "mcc-time" "1532825948" 156 | :Content-Type "application/json" 157 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" 158 | :mauth-version "v2"} 159 | :url "https://www.mdsol.com/api/v2/testing1" 160 | :method :delete 161 | :body "\"\"" 162 | :throw-exceptions false 163 | :as :auto}) 164 | 165 | (def additional-headers {:Content-Type "application/json" 166 | :Authorization "da2-3ubdc4ekk5dw5n2dvwjumet3fq" 167 | :mauth-version "v2"}) 168 | 169 | (def mock-payload (-> {:a {:b 123}} 170 | json/write-str)) 171 | 172 | (deftest header-test 173 | (get-credentials) 174 | (testing "It should make a valid GET request." 175 | (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing")] 176 | (is (= mock-get get-response)))) 177 | 178 | (testing "It should make a valid GET request with a querystring." 179 | (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing?testABCD=1234")] 180 | (is (= mock-get-with-qs get-response)))) 181 | 182 | (testing "It should make a valid POST request." 183 | (let [post-response (post! "https://www.mdsol.com" "/api/v2/testing1" mock-payload)] 184 | (is (= mock-post post-response)))) 185 | 186 | (testing "It should make a valid DELETE request." 187 | (let [delete-response (delete! "https://www.mdsol.com" "/api/v2/testing2")] 188 | (is (= mock-delete delete-response)))) 189 | 190 | (testing "It should make a valid PUT request." 191 | (let [put-response (put! "https://www.mdsol.com" "/api/v2/testing3" mock-payload)] 192 | (is (= mock-put put-response)))) 193 | 194 | (testing "It should make a valid POST request with v2 mauth header" 195 | (let [post-response (post! "https://www.mdsol.com" "/api/v2/testing1" mock-payload :additional-headers additional-headers 196 | :with-sni? false)] 197 | (is (= mock-post-v2 post-response)))) 198 | 199 | (testing "It should make a valid PUT request with v2 mauth header" 200 | (let [put-response (put! "https://www.mdsol.com" "/api/v2/testing1" mock-payload :additional-headers additional-headers 201 | :with-sni? false)] 202 | (is (= mock-put-v2 put-response)))) 203 | 204 | (testing "It should make a valid GET request with v2 mauth header" 205 | (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing1" :additional-headers additional-headers 206 | :with-sni? false)] 207 | (is (= mock-get-v2 get-response)))) 208 | 209 | (testing "It should make a valid GET request with query string and with v2 mauth header" 210 | 211 | (let [get-response (get! "https://www.mdsol.com" "/api/v2/testing1?testABCD=1234" :additional-headers additional-headers 212 | :with-sni? false)] 213 | (is (= mock-get-with-qs-v2 get-response)))) 214 | 215 | (testing "It should make a valid DELETE request with v2 mauth header" 216 | (let [delete-response (delete! "https://www.mdsol.com" "/api/v2/testing1" :additional-headers additional-headers 217 | :with-sni? false)] 218 | (is (= mock-delete-v2 delete-response))))) 219 | --------------------------------------------------------------------------------