├── .gitignore ├── CHANGES ├── LICENSE ├── README.markdown ├── build.xml ├── project.clj ├── src ├── oauth.clj └── oauth │ ├── client.clj │ ├── digest.clj │ └── signature.clj ├── test └── oauth │ ├── client_test.clj │ ├── client_twitter_test.clj │ ├── client_xauth_test.clj │ └── signature_test.clj └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.sw* 3 | lib 4 | classes 5 | target 6 | test/oauth/twitter_keys.clj 7 | *.jar 8 | pom.xml 9 | .lein-* 10 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | * 1.5.5 2 | 3 | - The user-approval-uri function now supports adding paramerers to the user approval URI. (danielsz) 4 | 5 | * 1.5.4 6 | 7 | - Updated Clojure, bouncycastle, and clj-http dependencies. 8 | 9 | * 1.5.3 (SNAPSHOT) 10 | 11 | - Updated dependencies. 12 | 13 | * 1.5.2 14 | 15 | - Support xAuth request tokens. 16 | 17 | * 1.5.1 18 | 19 | - Add missing dependency for OpenSSL PEM reader from bouncycastle. Regression introduced in 1.5.0. 20 | 21 | * 1.5.0 22 | 23 | - Add support for extra params to `request-token` used by Etsy. 24 | - Use Clojure 1.5 and clj-http 0.9.1 25 | 26 | * 1.4.1 27 | 28 | * 1.4.0 29 | 30 | - Use Clojure 1.4, Lein 2, and clj-http 0.5.3. 31 | 32 | * 1.2.9 33 | 34 | - Fixed user approval URL. Only oauth_token param is added to URL. 35 | 36 | * 1.2.8 37 | 38 | - Added xAuth support. 39 | 40 | * 1.2.7 41 | 42 | - Fixed bug where author didn't update CHANGES. 43 | - All authentication uses headers. 44 | 45 | * 1.2.4 46 | 47 | - Fixed missed URL decoding of form-encoded responses. 48 | - Removed server implementation for releases. 49 | 50 | * 1.2.3 51 | 52 | - Fixed timestamp bug causing 401's with Twitter. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Matt Revelle 2 | All rights reserved 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # OAuth support for Clojure # 2 | 3 | `clj-oauth` provides [OAuth](http://oauth.net) Client support for Clojure programs. 4 | 5 | # Installing # 6 | 7 | Add `[clj-oauth "1.5.5"]` as a Leiningen dependency to get the latest release. 8 | 9 | # Building # 10 | 11 | `lein jar` 12 | 13 | # Running Tests # 14 | 15 | Create a file test/oauth/twitter_keys.clj that contains the consumer key and secret. 16 | 17 | ``` clojure 18 | (def key "blah") 19 | (def secret "itsasecret") 20 | ``` 21 | 22 | `lein test` 23 | 24 | # Client Example # 25 | ``` clojure 26 | (require ['oauth.client :as 'oauth]) 27 | 28 | ;; Create a Consumer, in this case one to access Twitter. 29 | ;; Register an application at Twitter (https://dev.twitter.com/apps/new) 30 | ;; to obtain a Consumer token and token secret. 31 | (def consumer (oauth/make-consumer 32 | 33 | "https://api.twitter.com/oauth/request_token" 34 | "https://api.twitter.com/oauth/access_token" 35 | "https://api.twitter.com/oauth/authorize" 36 | :hmac-sha1)) 37 | 38 | ;; Fetch a request token that a OAuth User may authorize 39 | ;; 40 | ;; If you are using OAuth with a desktop application, a callback URI 41 | ;; is not required. 42 | (def request-token (oauth/request-token consumer )) 43 | 44 | ;; Send the User to this URI for authorization, they will be able 45 | ;; to choose the level of access to grant the application and will 46 | ;; then be redirected to the callback URI provided with the 47 | ;; request-token. 48 | (oauth/user-approval-uri consumer 49 | (:oauth_token request-token)) 50 | 51 | ;; Assuming the User has approved the request token, trade it for an access token. 52 | ;; The access token will then be used when accessing protected resources for the User. 53 | ;; 54 | ;; If the OAuth Service Provider provides a verifier, it should be included in the 55 | ;; request for the access token. See [Section 6.2.3](http://oauth.net/core/1.0a#rfc.section.6.2.3) of the OAuth specification 56 | ;; for more information. 57 | (def access-token-response (oauth/access-token consumer 58 | request-token 59 | )) 60 | 61 | ;; Each request to a protected resource must be signed individually. The 62 | ;; credentials are returned as a map of all OAuth parameters that must be 63 | ;; included with the request as either query parameters or in an 64 | ;; Authorization HTTP header. 65 | (def user-params {:status "posting from #clojure with #oauth"}) 66 | (def credentials (oauth/credentials consumer 67 | (:oauth_token access-token-response) 68 | (:oauth_token_secret access-token-response) 69 | :POST 70 | "https://api.twitter.com/1.1/statuses/update.json" 71 | user-params)) 72 | 73 | ;; Post with clj-http... 74 | (http/post "https://api.twitter.com/1.1/statuses/update.json" 75 | {:query-params (merge credentials user-params)} 76 | 77 | ;; ...or with clojure-twitter (http://github.com/mattrepl/clojure-twitter) 78 | (require 'twitter) 79 | 80 | (twitter/with-oauth consumer 81 | (:oauth_token access-token-response) 82 | (:oauth_token_secret access-token-response) 83 | (twitter/update-status "using clj-oauth with clojure-twitter")) 84 | ``` 85 | 86 | # Authors # 87 | 88 | Development funded by LikeStream LLC (Don Jackson and Shirish Andhare), see [http://www.likestream.org/opensource.html](http://www.likestream.org/opensource.html). 89 | 90 | Designed and developed by Matt Revelle. 91 | 92 | Contributions from Richard Newman. 93 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-oauth "1.5.6-SNAPSHOT" 2 | :url "https://github.com/drone-rites/clj-oauth" 3 | :license {:name "Simplified BSD License" 4 | :url "https://opensource.org/licenses/BSD-2-Clause" 5 | :distribution :repo} 6 | :description "OAuth support for Clojure" 7 | :dependencies [[org.clojure/clojure "1.10.3"] 8 | [commons-codec/commons-codec "1.15"] 9 | [org.bouncycastle/bcprov-jdk15on "1.70"] 10 | [org.bouncycastle/bcpkix-jdk15on "1.70"] 11 | [clj-http "3.12.3"]]) 12 | -------------------------------------------------------------------------------- /src/oauth.clj: -------------------------------------------------------------------------------- 1 | (ns oauth 2 | (:require oauth.client)) -------------------------------------------------------------------------------- /src/oauth/client.clj: -------------------------------------------------------------------------------- 1 | (ns 2 | #^{:author "Matt Revelle" 3 | :doc "OAuth client library for Clojure."} 4 | oauth.client 5 | (:require [oauth.digest :as digest] 6 | [oauth.signature :as sig] 7 | [clj-http.client :as httpclient]) 8 | (:use [clojure.string :only [join split upper-case]])) 9 | 10 | (defrecord #^{:doc "OAuth consumer"} 11 | Consumer [key secret request-uri 12 | access-uri authorize-uri signature-method]) 13 | (defn make-consumer 14 | "Make a consumer struct map." 15 | [key secret request-uri access-uri authorize-uri signature-method] 16 | (Consumer. 17 | key 18 | secret 19 | request-uri 20 | access-uri 21 | authorize-uri 22 | signature-method)) 23 | 24 | (defn user-approval-uri 25 | "Builds the URI to the Service Provider where the User will be prompted 26 | to approve the Consumer's access to their account. A map of extra parameters may be included." 27 | ([consumer token] 28 | (user-approval-uri consumer token {})) 29 | ([consumer token extra-params] 30 | (str (:authorize-uri consumer) 31 | "?" (httpclient/generate-query-string (merge {:oauth_token token} extra-params))))) 32 | 33 | (defn authorization-header 34 | "OAuth credentials formatted for the Authorization HTTP header." 35 | ([oauth-params] 36 | (str "OAuth " 37 | (join ", " 38 | (map (fn [[k v]] 39 | (str (-> k sig/as-str sig/url-encode) 40 | "=\"" (-> v sig/as-str sig/url-encode) "\"")) 41 | oauth-params)))) 42 | ([oauth-params realm] 43 | (authorization-header (assoc oauth-params :realm realm)))) 44 | 45 | (defn form-decode 46 | "Parse form-encoded bodies from OAuth responses." 47 | [s] 48 | (if s 49 | (into {} 50 | (map (fn [kv] 51 | (let [[k v] (split kv #"=") 52 | k (or k "") 53 | v (or v "")] 54 | [(keyword (sig/url-decode k)) (sig/url-decode v)])) 55 | (split s #"&"))))) 56 | 57 | (defn- check-success-response [m] 58 | (let [code (:status m)] 59 | (if (or (< code 200) 60 | (>= code 300)) 61 | (throw (new Exception (str "Got non-success code: " code ". " 62 | "Content: " (:body m)))) 63 | m))) 64 | 65 | (defn build-request 66 | "Construct request from prepared paramters." 67 | [oauth-params & [form-params]] 68 | (let [req (merge 69 | {:headers {"Authorization" (authorization-header 70 | oauth-params)}} 71 | (when form-params {:form-params form-params}))] 72 | req)) 73 | 74 | (defn post-request-body-decoded [url & [req]] 75 | (form-decode 76 | (:body (check-success-response 77 | (httpclient/post url req))))) 78 | 79 | (defn credentials 80 | "Return authorization credentials needed for access to protected resources. 81 | The key-value pairs returned as a map will need to be added to the 82 | Authorization HTTP header or added as query parameters to the request." 83 | ([consumer token token-secret request-method request-uri & [request-params]] 84 | (let [unsigned-oauth-params (sig/oauth-params consumer 85 | (sig/rand-str 30) 86 | (sig/msecs->secs (System/currentTimeMillis)) 87 | token) 88 | unsigned-params (merge request-params 89 | unsigned-oauth-params) 90 | signature (sig/sign consumer 91 | (sig/base-string (-> request-method 92 | sig/as-str 93 | upper-case) 94 | request-uri 95 | unsigned-params) 96 | token-secret)] 97 | (assoc unsigned-oauth-params :oauth_signature signature)))) 98 | 99 | (defn build-oauth-token-request 100 | "Used to build actual OAuth request." 101 | ([consumer uri unsigned-oauth-params & [extra-params token-secret]] 102 | (let [signature (sig/sign consumer 103 | (sig/base-string "POST" uri (merge unsigned-oauth-params extra-params)) 104 | token-secret) 105 | oauth-params (assoc unsigned-oauth-params :oauth_signature signature)] 106 | (build-request oauth-params extra-params)))) 107 | 108 | (defn request-token 109 | "Fetch request token for the consumer." 110 | ([consumer] 111 | (request-token consumer "oob" nil)) 112 | ([consumer callback-uri] 113 | (request-token consumer callback-uri nil)) 114 | ([consumer callback-uri extra-params] 115 | (let [unsigned-params (-> (sig/oauth-params consumer 116 | (sig/rand-str 30) 117 | (sig/msecs->secs (System/currentTimeMillis))) 118 | (assoc :oauth_callback callback-uri))] 119 | (post-request-body-decoded (:request-uri consumer) 120 | (build-oauth-token-request consumer 121 | (:request-uri consumer) 122 | unsigned-params 123 | extra-params))))) 124 | 125 | (defn access-token 126 | "Exchange a request token for an access token. 127 | When provided with two arguments, this function operates as per OAuth 1.0. 128 | With three arguments, a verifier is used." 129 | ([consumer request-token] 130 | (access-token consumer request-token nil)) 131 | ([consumer request-token verifier] 132 | (let [unsigned-oauth-params (if verifier 133 | (sig/oauth-params consumer 134 | (sig/rand-str 30) 135 | (sig/msecs->secs (System/currentTimeMillis)) 136 | (:oauth_token request-token) 137 | verifier) 138 | (sig/oauth-params consumer 139 | (sig/rand-str 30) 140 | (sig/msecs->secs (System/currentTimeMillis)) 141 | (:oauth_token 142 | request-token))) 143 | token-secret (:oauth_token_secret request-token)] 144 | (post-request-body-decoded (:access-uri consumer) 145 | (build-oauth-token-request consumer 146 | (:access-uri consumer) 147 | unsigned-oauth-params 148 | nil 149 | token-secret))))) 150 | (defn build-xauth-access-token-request 151 | ([consumer username password nonce timestamp] 152 | (build-xauth-access-token-request consumer nil username password nonce timestamp)) 153 | ([consumer {token :oauth_token secret :oauth_token_secret} username password nonce timestamp] 154 | (let [oauth-params (if token 155 | (sig/oauth-params consumer nonce timestamp token) 156 | (sig/oauth-params consumer nonce timestamp)) 157 | post-params {:x_auth_username username 158 | :x_auth_password password 159 | :x_auth_mode "client_auth"} 160 | signature-base (sig/base-string "POST" 161 | (:access-uri consumer) 162 | (merge oauth-params 163 | post-params)) 164 | signature (if secret (sig/sign consumer signature-base secret) (sig/sign consumer signature-base)) 165 | params (assoc oauth-params 166 | :oauth_signature signature)] 167 | (build-request params post-params)))) 168 | 169 | (defn refresh-token 170 | "Exchange an expired access token for a new access token." 171 | ([consumer expired-token] 172 | (refresh-token consumer expired-token nil)) 173 | ([consumer expired-token verifier] 174 | (let [base-oauth-params (if verifier 175 | (sig/oauth-params consumer 176 | (sig/rand-str 30) 177 | (sig/msecs->secs (System/currentTimeMillis)) 178 | (:oauth_token expired-token) 179 | verifier) 180 | (sig/oauth-params consumer 181 | (sig/rand-str 30) 182 | (sig/msecs->secs (System/currentTimeMillis)) 183 | (:oauth_token expired-token))) 184 | unsigned-oauth-params (assoc base-oauth-params 185 | :oauth_session_handle (:oauth_session_handle expired-token))] 186 | (post-request-body-decoded (:access-uri consumer) 187 | (build-oauth-token-request consumer 188 | (:access-uri consumer) 189 | unsigned-oauth-params 190 | nil 191 | (:oauth_token_secret expired-token)))))) 192 | 193 | (defn xauth-access-token 194 | "Request an access token with a username and password with xAuth." 195 | [consumer username password] 196 | (post-request-body-decoded (:access-uri consumer) 197 | (build-xauth-access-token-request consumer 198 | username 199 | password 200 | (sig/rand-str 30) 201 | (sig/msecs->secs (System/currentTimeMillis))))) 202 | -------------------------------------------------------------------------------- /src/oauth/digest.clj: -------------------------------------------------------------------------------- 1 | (ns oauth.digest 2 | (:import (javax.crypto Mac) 3 | (javax.crypto.spec SecretKeySpec))) 4 | 5 | (defn hmac-sign 6 | "Calculate HMAC signature for given data." 7 | [^String key ^String data ^String hmac-algo] 8 | (let [signing-key (SecretKeySpec. (.getBytes key) hmac-algo) 9 | mac (doto (Mac/getInstance hmac-algo) (.init signing-key))] 10 | (String. (org.apache.commons.codec.binary.Base64/encodeBase64 11 | (.doFinal mac (.getBytes data))) 12 | "UTF-8"))) 13 | -------------------------------------------------------------------------------- /src/oauth/signature.clj: -------------------------------------------------------------------------------- 1 | (ns 2 | #^{:author "Matt Revelle" 3 | :doc "OAuth client library for Clojure."} 4 | oauth.signature 5 | (:import org.apache.commons.codec.binary.Base64 6 | org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter) 7 | (:require [oauth.digest :as digest]) 8 | (:use [clojure.string :only [join]])) 9 | 10 | (declare rand-str 11 | base-string 12 | sign 13 | url-encode 14 | oauth-params) 15 | 16 | (defn- named? [a] 17 | (instance? clojure.lang.Named a)) 18 | 19 | (defn as-str [a] 20 | (if (named? a) 21 | (name a) 22 | (str a))) 23 | 24 | (def secure-random (java.security.SecureRandom/getInstance "SHA1PRNG")) 25 | 26 | (defn rand-str 27 | "Random string for OAuth requests." 28 | [length] 29 | (. (new BigInteger (int (* 5 length)) ^java.util.Random secure-random) toString 32)) 30 | 31 | (defn msecs->secs 32 | "Convert milliseconds to seconds." 33 | [millis] 34 | (int (/ millis 1000))) 35 | 36 | (def signature-methods {:hmac-sha1 "HMAC-SHA1" 37 | :hmac-sha256 "HMAC-SHA256" 38 | :rsa-sha1 "RSA-SHA1" 39 | :plaintext "PLAINTEXT"}) 40 | 41 | (defn url-form-encode [params] 42 | (join "&" (map (fn [[k v]] 43 | (str (url-encode (as-str k)) 44 | "=" (url-encode (as-str v)))) params ))) 45 | (defn base-string 46 | ([method base-url c t params] 47 | (base-string method base-url 48 | (assoc params 49 | :oauth_consumer_key (:key c) 50 | :oauth_token (:token t) 51 | :oauth_signature_method (or (params :oauth_signature_method) 52 | (signature-methods (:signature-method c))) 53 | :oauth_version "1.0"))) 54 | ([method base-url params] 55 | (join "&" [method 56 | (url-encode base-url) 57 | (url-encode (url-form-encode (sort params)))]))) 58 | 59 | (defmulti sign 60 | "Sign a base string for authentication." 61 | {:arglists '([consumer base-string & [token-secret]])} 62 | (fn [c & r] (:signature-method c))) 63 | 64 | (defmethod sign :hmac-sha1 65 | [c base-string & [token-secret]] 66 | (let [key (str (url-encode (:secret c)) "&" (url-encode (or token-secret "")))] 67 | (digest/hmac-sign key base-string "HmacSHA1"))) 68 | 69 | (defmethod sign :hmac-sha256 70 | [c base-string & [token-secret]] 71 | (let [key (str (url-encode (:secret c)) "&" (url-encode (or token-secret "")))] 72 | (digest/hmac-sign key base-string "HmacSHA256"))) 73 | 74 | (defmethod sign :plaintext 75 | [c base-string & [token-secret]] 76 | (str (url-encode (:secret c)) "&" (url-encode (or token-secret "")))) 77 | 78 | (def ^:private pem-converter 79 | (doto (JcaPEMKeyConverter.) 80 | (.setProvider "BC"))) 81 | 82 | (defmethod sign :rsa-sha1 83 | [c ^String base-string & [token-secret]] 84 | (java.security.Security/addProvider 85 | (org.bouncycastle.jce.provider.BouncyCastleProvider.)) 86 | (let [key-pair (-> (:secret c) 87 | java.io.StringReader. 88 | org.bouncycastle.openssl.PEMParser. 89 | .readObject) 90 | private-key (-> ^JcaPEMKeyConverter pem-converter 91 | (.getKeyPair key-pair) 92 | .getPrivate) 93 | signer (doto (java.security.Signature/getInstance "SHA1withRSA" "BC") 94 | (.initSign private-key (java.security.SecureRandom.)) 95 | (.update (.getBytes base-string))) 96 | raw-sig (.sign signer)] 97 | (String. (Base64/encodeBase64 raw-sig)))) 98 | 99 | (defn verify [sig c base-string & [token-secret]] 100 | (let [token-secret (url-encode (or token-secret ""))] 101 | (= sig (sign c base-string token-secret)))) 102 | 103 | (defn url-encode 104 | "The java.net.URLEncoder class encodes for application/x-www-form-urlencoded, but OAuth 105 | requires RFC 3986 encoding." 106 | [s] 107 | (-> (java.net.URLEncoder/encode s "UTF-8") 108 | (.replace "+" "%20") 109 | (.replace "*" "%2A") 110 | (.replace "%7E" "~"))) 111 | 112 | (defn url-decode 113 | "The java.net.URLEncoder class encodes for application/x-www-form-urlencoded, but OAuth 114 | requires RFC 3986 encoding." 115 | [s] 116 | (java.net.URLDecoder/decode s "UTF-8")) 117 | 118 | (defn oauth-params 119 | "Build a map of parameters needed for OAuth requests." 120 | ([consumer nonce timestamp] 121 | {:oauth_consumer_key (:key consumer) 122 | :oauth_signature_method (signature-methods (:signature-method consumer)) 123 | :oauth_timestamp timestamp 124 | :oauth_nonce nonce 125 | :oauth_version "1.0"}) 126 | ([consumer nonce timestamp token] 127 | (assoc (oauth-params consumer nonce timestamp) 128 | :oauth_token token)) 129 | ([consumer nonce timestamp token verifier] 130 | (assoc (oauth-params consumer nonce timestamp token) 131 | :oauth_verifier (str verifier)))) 132 | -------------------------------------------------------------------------------- /test/oauth/client_test.clj: -------------------------------------------------------------------------------- 1 | (ns oauth.client-test 2 | (:require [oauth.client :as oc] 3 | [oauth.signature :as sig] 4 | :reload-all) 5 | (:use clojure.test 6 | [clojure.pprint :only [pprint]])) 7 | 8 | (deftest ^{:doc "Test creation of authorization header for request_token access."} 9 | request-access-authorization-header 10 | (let [c (oc/make-consumer "GDdmIQH6jhtmLUypg82g" 11 | "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98" 12 | "https://api.twitter.com/oauth/request_token" 13 | "https://api.twitter.com/oauth/access_token" 14 | "https://api.twitter.com/oauth/authorize" 15 | :hmac-sha1) 16 | ;; Ensure that the params from Twitter example are used. 17 | unsigned-params (merge (sig/oauth-params c "QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk" 1272323042) 18 | {:oauth_callback "http://localhost:3005/the_dance/process_callback?service_provider_id=11" 19 | :oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 20 | :oauth_signature_method "HMAC-SHA1" 21 | :oauth_version "1.0"}) 22 | signature (sig/sign c (sig/base-string "POST" 23 | (:request-uri c) 24 | unsigned-params)) 25 | params (assoc unsigned-params 26 | :oauth_signature signature)] 27 | ;; Can't easily test this since params are in undefined order in header. 28 | (is (= (oc/authorization-header (sort params)) 29 | "OAuth oauth_callback=\"http%3A%2F%2Flocalhost%3A3005%2Fthe_dance%2Fprocess_callback%3Fservice_provider_id%3D11\", oauth_consumer_key=\"GDdmIQH6jhtmLUypg82g\", oauth_nonce=\"QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk\", oauth_signature=\"8wUi7m5HFQy76nowoCThusfgB%2BQ%3D\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1272323042\", oauth_version=\"1.0\"")))) 30 | 31 | (deftest ^{:doc "Test creation of authorization header for access_token request"} 32 | access-token-authorization-header 33 | (let [c (oc/make-consumer "GDdmIQH6jhtmLUypg82g" 34 | "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98" 35 | "https://api.twitter.com/oauth/request_token" 36 | "https://api.twitter.com/oauth/access_token" 37 | "https://api.twitter.com/oauth/authorize" 38 | :hmac-sha1) 39 | ;; Ensure that the params from Twitter example are used. 40 | unsigned-params (merge (sig/oauth-params c "9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8" 1272323047) 41 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 42 | :oauth_signature_method "HMAC-SHA1" 43 | :oauth_token "8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc" 44 | :oauth_verifier "pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY" 45 | :oauth_version "1.0"}) 46 | signature (sig/sign c 47 | (sig/base-string "POST" 48 | (:access-uri c) 49 | unsigned-params) 50 | "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA") ;; token secret 51 | params (assoc unsigned-params 52 | :oauth_signature signature)] 53 | (is (= (oc/authorization-header (sort params)) 54 | "OAuth oauth_consumer_key=\"GDdmIQH6jhtmLUypg82g\", oauth_nonce=\"9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8\", oauth_signature=\"PUw%2FdHA4fnlJYM6RhXk5IU%2F0fCc%3D\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1272323047\", oauth_token=\"8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc\", oauth_verifier=\"pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY\", oauth_version=\"1.0\"")))) 55 | 56 | (deftest ^{:doc "Test creation of approval URL"} 57 | user-approval-uri 58 | (let [c (oc/make-consumer "GDdmIQH6jhtmLUypg82g" 59 | "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98" 60 | "https://api.twitter.com/oauth/request_token" 61 | "https://api.twitter.com/oauth/access_token" 62 | "https://api.twitter.com/oauth/authorize" 63 | :hmac-sha1) 64 | t "nnch734d00sl2jdk"] 65 | ;; The approval URL should only use the :oauth_token in the User approval URI 66 | (is (= "https://api.twitter.com/oauth/authorize?oauth_token=nnch734d00sl2jdk" 67 | (oc/user-approval-uri c t))) 68 | (is (= "https://api.twitter.com/oauth/authorize?oauth_token=nnch734d00sl2jdk&extra=foo" 69 | (oc/user-approval-uri c t {:extra "foo"}))))) 70 | 71 | (deftest ^{:doc "Test creation of authorization header for refresh access_token request"} 72 | refresh-token-authorization-header 73 | (let [c (oc/make-consumer "GDdmIQH6jhtmLUypg82g" 74 | "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98" 75 | "https://api.twitter.com/oauth/request_token" 76 | "https://api.twitter.com/oauth/access_token" 77 | "https://api.twitter.com/oauth/authorize" 78 | :hmac-sha1) 79 | unsigned-params (merge (sig/oauth-params c "9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8" 1272323047) 80 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 81 | :oauth_nonce "9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8" 82 | :oauth_signature_method "HMAC-SHA1" 83 | :oauth_token "8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc" 84 | :oauth_timestamp "1272323047" 85 | :oauth_version "1.0"} 86 | {:oauth_session_handle "5a10ddsqoqo2rfi"}) 87 | signature (sig/sign c (sig/base-string "POST" 88 | (:request-uri c) 89 | unsigned-params)) 90 | params (assoc unsigned-params 91 | :oauth_signature signature)] 92 | (is (= (oc/authorization-header (sort params)) 93 | "OAuth oauth_consumer_key=\"GDdmIQH6jhtmLUypg82g\", oauth_nonce=\"9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8\", oauth_session_handle=\"5a10ddsqoqo2rfi\", oauth_signature=\"f15S84zVZ96f9PwAJrBHq28KIF4%3D\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1272323047\", oauth_token=\"8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc\", oauth_version=\"1.0\"")))) 94 | -------------------------------------------------------------------------------- /test/oauth/client_twitter_test.clj: -------------------------------------------------------------------------------- 1 | (ns oauth.client-twitter-test 2 | (:refer-clojure :exclude [key]) 3 | (:require [oauth.client :as oc]) 4 | (:use clojure.test) 5 | (:load "twitter_keys")) 6 | 7 | (def consumer-hmac-sha1 (oc/make-consumer key 8 | secret 9 | "https://api.twitter.com/oauth/request_token" 10 | "https://api.twitter.com/oauth/access_token" 11 | "https://api.twitter.com/oauth/authorize" 12 | :hmac-sha1)) 13 | (deftest 14 | #^{:doc "Test requesting a token from Twitter. 15 | Considered to pass if no exception is thrown."} 16 | hmac-sha1-request-token-test 17 | (oc/request-token consumer-hmac-sha1)) 18 | 19 | (deftest 20 | #^{:doc "Considered to pass if no exception is thrown."} 21 | hmac-sha1-user-approval-uri-test 22 | (is (instance? String (oc/user-approval-uri consumer-hmac-sha1 (:oauth_token (oc/request-token consumer-hmac-sha1)))))) 23 | 24 | (def consumer-hmac-sha256 (oc/make-consumer key 25 | secret 26 | "https://api.twitter.com/oauth/request_token" 27 | "https://api.twitter.com/oauth/access_token" 28 | "https://api.twitter.com/oauth/authorize" 29 | :hmac-sha256)) 30 | (deftest 31 | #^{:doc "Test requesting a token from Twitter. 32 | Considered to pass if no exception is thrown."} 33 | hmac-sha256-request-token-test 34 | (oc/request-token consumer-hmac-sha256)) 35 | 36 | (deftest 37 | #^{:doc "Considered to pass if no exception is thrown."} 38 | hmac-sha256-user-approval-uri-test 39 | (is (instance? String (oc/user-approval-uri consumer-hmac-sha256 (:oauth_token (oc/request-token consumer-hmac-sha256)))))) 40 | 41 | #_(deftest 42 | #^{:doc "Considered to pass if no exception is thrown."} 43 | access-token 44 | (let [request-token (oc/request-token consumer)] 45 | (oc/access-token consumer request-token ...verifier...))) 46 | -------------------------------------------------------------------------------- /test/oauth/client_xauth_test.clj: -------------------------------------------------------------------------------- 1 | (ns oauth.client-xauth-test 2 | (:require [oauth.client :as oc] 3 | [oauth.signature :as sig]) 4 | (:use clojure.test)) 5 | 6 | (def consumer (oc/make-consumer "JvyS7DO2qd6NNTsXJ4E7zA" 7 | "9z6157pUbOBqtbm0A0q4r29Y2EYzIHlUwbF4Cl9c" 8 | "https://api.twitter.com/oauth/request_token" 9 | "https://api.twitter.com/oauth/access_token" 10 | "https://api.twitter.com/oauth/authorize" 11 | :hmac-sha1)) 12 | 13 | (deftest xauth-base-string-test 14 | (is (= "POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Faccess_token&oauth_consumer_key%3DJvyS7DO2qd6NNTsXJ4E7zA%26oauth_nonce%3D6AN2dKRzxyGhmIXUKSmp1JcB4pckM8rD3frKMTmVAo%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1284565601%26oauth_version%3D1.0%26x_auth_mode%3Dclient_auth%26x_auth_password%3Dtwitter-xauth%26x_auth_username%3Doauth_test_exec" 15 | (sig/base-string "POST" "https://api.twitter.com/oauth/access_token" 16 | (merge {:x_auth_username "oauth_test_exec" 17 | :x_auth_password "twitter-xauth" 18 | :x_auth_mode "client_auth"} 19 | {:oauth_consumer_key "JvyS7DO2qd6NNTsXJ4E7zA" 20 | :oauth_nonce "6AN2dKRzxyGhmIXUKSmp1JcB4pckM8rD3frKMTmVAo" 21 | :oauth_timestamp "1284565601" 22 | :oauth_version "1.0" 23 | :oauth_signature_method "HMAC-SHA1"}))))) 24 | 25 | (deftest build-xauth-access-token-request-test 26 | (is (= {:form-params {:x_auth_username "oauth_test_exec", 27 | :x_auth_password "twitter-xauth", 28 | :x_auth_mode "client_auth"}, 29 | :headers {"Authorization" "OAuth oauth_consumer_key=\"JvyS7DO2qd6NNTsXJ4E7zA\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1284565601\", oauth_nonce=\"6AN2dKRzxyGhmIXUKSmp1JcB4pckM8rD3frKMTmVAo\", oauth_version=\"1.0\", oauth_signature=\"1L1oXQmawZAkQ47FHLwcOV%2Bkjwc%3D\""}} 30 | (oc/build-xauth-access-token-request consumer 31 | "oauth_test_exec" 32 | "twitter-xauth" 33 | "6AN2dKRzxyGhmIXUKSmp1JcB4pckM8rD3frKMTmVAo" 34 | 1284565601)))) 35 | -------------------------------------------------------------------------------- /test/oauth/signature_test.clj: -------------------------------------------------------------------------------- 1 | (ns oauth.signature-test 2 | (:require [oauth.client :as oc] 3 | [oauth.signature :as sig] :reload-all) 4 | (:use clojure.test)) 5 | 6 | (def twitter-req-params 7 | {:oauth_callback "http://localhost:3005/the_dance/process_callback?service_provider_id=11" 8 | :oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 9 | :oauth_nonce "QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk" 10 | :oauth_signature_method "HMAC-SHA1" 11 | :oauth_timestamp "1272323042" 12 | :oauth_version "1.0"}) 13 | 14 | (deftest 15 | as-str-function 16 | (is (= (sig/as-str :hello) "hello")) 17 | (is (= (sig/as-str 3412) "3412")) 18 | (is (= (sig/as-str 'hello) "hello"))) 19 | 20 | (deftest 21 | signature-methods 22 | (is (= (sig/signature-methods :hmac-sha1) "HMAC-SHA1"))) 23 | 24 | (deftest 25 | signature-base-string 26 | (is (= (sig/base-string "GET" 27 | "http://photos.example.net/photos" 28 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 29 | :oauth_token "nnch734d00sl2jdk" 30 | :oauth_signature_method "HMAC-SHA1" 31 | :oauth_timestamp "1191242096" 32 | :oauth_nonce "kllo9940pd9333jh" 33 | :oauth_version "1.0" 34 | :file "vacation.jpg" 35 | :size "original"}) 36 | "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal")) 37 | 38 | (is (= (sig/base-string "GET" 39 | "http://photos.example.net/photos" 40 | {:key "dpf43f3p2l4k3l03" 41 | :secret "kd94hf93k423kf44" 42 | :signature-method :hmac-sha1} 43 | {:token "nnch734d00sl2jdk" 44 | :secret "pfkkdhi9sl3r4s00"} 45 | {:oauth_timestamp "1191242096" 46 | :oauth_nonce "kllo9940pd9333jh" 47 | :file "vacation.jpg" 48 | :size "original"}) 49 | "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal")) 50 | 51 | (is (= (sig/base-string "POST" 52 | "https://api.twitter.com/oauth/request_token" 53 | {:oauth_callback "http://localhost:3005/the_dance/process_callback?service_provider_id=11" 54 | :oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 55 | :oauth_nonce "QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk" 56 | :oauth_signature_method "HMAC-SHA1" 57 | :oauth_timestamp "1272323042" 58 | :oauth_version "1.0"}) 59 | "POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&oauth_callback%3Dhttp%253A%252F%252Flocalhost%253A3005%252Fthe_dance%252Fprocess_callback%253Fservice_provider_id%253D11%26oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonce%3DQP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272323042%26oauth_version%3D1.0")) 60 | 61 | (is (= (sig/base-string "POST" 62 | "https://api.twitter.com/oauth/access_token" 63 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 64 | :oauth_nonce "9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8" 65 | :oauth_signature_method "HMAC-SHA1" 66 | :oauth_token "8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc" 67 | :oauth_timestamp "1272323047" 68 | :oauth_verifier "pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY" 69 | :oauth_version "1.0"}) 70 | "POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Faccess_token&oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonce%3D9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272323047%26oauth_token%3D8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc%26oauth_verifier%3DpDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY%26oauth_version%3D1.0")) 71 | 72 | (is (= (sig/base-string "POST" 73 | "http://api.twitter.com/1/statuses/update.json" 74 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 75 | :oauth_nonce "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y" 76 | :oauth_signature_method "HMAC-SHA1" 77 | :oauth_timestamp "1272325550" 78 | :oauth_version "1.0" 79 | :oauth_token "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw" 80 | :status "setting up my twitter 私のさえずりを設定する"}) 81 | "POST&http%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonce%3DoElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272325550%26oauth_token%3D819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw%26oauth_version%3D1.0%26status%3Dsetting%2520up%2520my%2520twitter%2520%25E7%25A7%2581%25E3%2581%25AE%25E3%2581%2595%25E3%2581%2588%25E3%2581%259A%25E3%2582%258A%25E3%2582%2592%25E8%25A8%25AD%25E5%25AE%259A%25E3%2581%2599%25E3%2582%258B")) 82 | 83 | (is (= (sig/base-string "POST" 84 | "http://api.twitter.com/1/statuses/update.json" 85 | {:key "GDdmIQH6jhtmLUypg82g" 86 | :signature-method :hmac-sha1} 87 | {:token "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw"} 88 | {:oauth_nonce "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y" 89 | :oauth_timestamp "1272325550" 90 | :oauth_version "1.0" 91 | :status "setting up my twitter 私のさえずりを設定する"}) 92 | "POST&http%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&oauth_consumer_key%3DGDdmIQH6jhtmLUypg82g%26oauth_nonce%3DoElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1272325550%26oauth_token%3D819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw%26oauth_version%3D1.0%26status%3Dsetting%2520up%2520my%2520twitter%2520%25E7%25A7%2581%25E3%2581%25AE%25E3%2581%2595%25E3%2581%2588%25E3%2581%259A%25E3%2582%258A%25E3%2582%2592%25E8%25A8%25AD%25E5%25AE%259A%25E3%2581%2599%25E3%2582%258B"))) 93 | 94 | (deftest 95 | #^{:doc "Test hmac-sha1 signing of a request."} 96 | hmac-sha1-signature 97 | 98 | (is (= (sig/sign {:key "dpf43f3p2l4k3l03" 99 | :secret "kd94hf93k423kf44" 100 | :signature-method :hmac-sha1} 101 | (sig/base-string "GET" 102 | "http://photos.example.net/photos" 103 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 104 | :oauth_token "nnch734d00sl2jdk" 105 | :oauth_signature_method "HMAC-SHA1" 106 | :oauth_timestamp "1191242096" 107 | :oauth_nonce "kllo9940pd9333jh" 108 | :oauth_version "1.0" 109 | :file "vacation.jpg" 110 | :size "original"}) 111 | "pfkkdhi9sl3r4s00") 112 | "tR3+Ty81lMeYAr/Fid0kMTYa/WM=")) 113 | 114 | ;; Taken from Twitter dev example. 115 | (is (= (sig/sign {:signature-method :hmac-sha1 116 | :secret "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"} 117 | (sig/base-string "POST" 118 | "https://api.twitter.com/oauth/request_token" 119 | {:oauth_callback "http://localhost:3005/the_dance/process_callback?service_provider_id=11" 120 | :oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 121 | :oauth_nonce "QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk" 122 | :oauth_signature_method "HMAC-SHA1" 123 | :oauth_timestamp "1272323042" 124 | :oauth_version "1.0"})) 125 | "8wUi7m5HFQy76nowoCThusfgB+Q=")) 126 | 127 | (is (= (sig/sign {:signature-method :hmac-sha1 128 | :secret "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"} 129 | (sig/base-string "POST" 130 | "https://api.twitter.com/oauth/access_token" 131 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 132 | :oauth_nonce "9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8" 133 | :oauth_signature_method "HMAC-SHA1" 134 | :oauth_token "8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc" 135 | :oauth_timestamp "1272323047" 136 | :oauth_verifier "pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY" 137 | :oauth_version "1.0"}) 138 | "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA") 139 | "PUw/dHA4fnlJYM6RhXk5IU/0fCc=")) 140 | 141 | (is (= (sig/sign {:signature-method :hmac-sha1 142 | :secret "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"} 143 | (sig/base-string "POST" 144 | "http://api.twitter.com/1/statuses/update.json" 145 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 146 | :oauth_nonce "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y" 147 | :oauth_signature_method "HMAC-SHA1" 148 | :oauth_timestamp "1272325550" 149 | :oauth_token "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw" 150 | :oauth_version "1.0" 151 | :status "setting up my twitter 私のさえずりを設定する"}) 152 | "J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA") 153 | "yOahq5m0YjDDjfjxHaXEsW9D+X0="))) 154 | 155 | (deftest 156 | #^{:doc "Test hmac-sha256 signing of a request."} 157 | hmac-sha256-signature 158 | 159 | (is (= (sig/sign {:key "dpf43f3p2l4k3l03" 160 | :secret "kd94hf93k423kf44" 161 | :signature-method :hmac-sha256} 162 | (sig/base-string "GET" 163 | "http://photos.example.net/photos" 164 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 165 | :oauth_token "nnch734d00sl2jdk" 166 | :oauth_signature_method "HMAC-SHA256" 167 | :oauth_timestamp "1191242096" 168 | :oauth_nonce "kllo9940pd9333jh" 169 | :oauth_version "1.0" 170 | :file "vacation.jpg" 171 | :size "original"}) 172 | "pfkkdhi9sl3r4s00") 173 | "WVPzl1j6ZsnkIjWr7e3OZ3jkenL57KwaLFhYsroX1hg=")) 174 | 175 | ;; Taken from Twitter dev example. 176 | (is (= (sig/sign {:signature-method :hmac-sha256 177 | :secret "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"} 178 | (sig/base-string "POST" 179 | "https://api.twitter.com/oauth/request_token" 180 | {:oauth_callback "http://localhost:3005/the_dance/process_callback?service_provider_id=11" 181 | :oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 182 | :oauth_nonce "QP70eNmVz8jvdPevU3oJD2AfF7R7odC2XJcn4XlZJqk" 183 | :oauth_signature_method "HMAC-SHA256" 184 | :oauth_timestamp "1272323042" 185 | :oauth_version "1.0"})) 186 | "N0KBpiPbuPIMx2B77eIg7tNfGNF81iq3bcO9RO6lH+k=")) 187 | 188 | (is (= (sig/sign {:signature-method :hmac-sha256 189 | :secret "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"} 190 | (sig/base-string "POST" 191 | "https://api.twitter.com/oauth/access_token" 192 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 193 | :oauth_nonce "9zWH6qe0qG7Lc1telCn7FhUbLyVdjEaL3MO5uHxn8" 194 | :oauth_signature_method "HMAC-SHA256" 195 | :oauth_token "8ldIZyxQeVrFZXFOZH5tAwj6vzJYuLQpl0WUEYtWc" 196 | :oauth_timestamp "1272323047" 197 | :oauth_verifier "pDNg57prOHapMbhv25RNf75lVRd6JDsni1AJJIDYoTY" 198 | :oauth_version "1.0"}) 199 | "x6qpRnlEmW9JbQn4PQVVeVG8ZLPEx6A0TOebgwcuA") 200 | "y7S9eUhA0tC9/YfRzCPqkg3/bUdYRDpZ93Xi51AvhjQ=")) 201 | 202 | (is (= (sig/sign {:signature-method :hmac-sha256 203 | :secret "MCD8BKwGdgPHvAuvgvz4EQpqDAtx89grbuNMRd7Eh98"} 204 | (sig/base-string "POST" 205 | "http://api.twitter.com/1/statuses/update.json" 206 | {:oauth_consumer_key "GDdmIQH6jhtmLUypg82g" 207 | :oauth_nonce "oElnnMTQIZvqvlfXM56aBLAf5noGD0AQR3Fmi7Q6Y" 208 | :oauth_signature_method "HMAC-SHA256" 209 | :oauth_timestamp "1272325550" 210 | :oauth_token "819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw" 211 | :oauth_version "1.0" 212 | :status "setting up my twitter 私のさえずりを設定する"}) 213 | "J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA") 214 | "xYhKjozxc3NYef7C26WU+gORdhEURdZRxSDzRttEKH0="))) 215 | 216 | (deftest rsa-sha1-signature 217 | (let [c {:secret 218 | "-----BEGIN RSA PRIVATE KEY----- 219 | MIIBOwIBAAJBAPwrtgkaYAbp/xzfBzcsZR/ADW1ZVsRG6JOou8AcFM/gvuPOxXVk 220 | 5zhs+rTk19UO53XO9cHOFd4HwndY/Y7tcOMCAwEAAQJBAKqRQnML1RI4Kqgzr2TB 221 | cbE1LZ/eQxNGR0DBbCV4mRc1vRAZ6Y/lbVEm7snA6CYsTALr5ajoCQgL3DReCMhj 222 | rbkCIQD+sDIWPIx+zCLJ+1mFk9dt48EFFQNHjfPhMXdeIohetwIhAP14MjN/4jlj 223 | V8d9H0WILt/1xCHcneNojYACVGxRZ9M1AiARwoObnVlGtkFuyEIz2F1bYlhhXFfA 224 | M5vgBi0GuW28/QIgGU83NA1A+ZoB2dmUlczTYWmY/Aibe2mlN3MEGwzF4UECIQCa 225 | YRd7U3DJo60QR8zRepaqOILLwtkpxoauNOYx35vF/Q== 226 | -----END RSA PRIVATE KEY-----" 227 | :key 228 | "-----BEGIN PUBLIC KEY----- 229 | MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPwrtgkaYAbp/xzfBzcsZR/ADW1ZVsRG 230 | 6JOou8AcFM/gvuPOxXVk5zhs+rTk19UO53XO9cHOFd4HwndY/Y7tcOMCAwEAAQ== 231 | -----END PUBLIC KEY-----" 232 | :signature-method :rsa-sha1}] 233 | (is (= (str "bLsgZgdgVNNmL0Gc7yjrG6L8YRpYOu1nB1cSKZVIcpjWjWK6GYLo7p9m0/" 234 | "KTWtS0+1PfdVCSG6CY9Vc1pI59Sg==") 235 | (sig/sign c 236 | (sig/base-string "POST" 237 | "https://photos.example.net/request_token" 238 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 239 | :oauth_signature_method "RSA-SHA1" 240 | :oauth_timestamp "1191242090" 241 | :oauth_nonce "hsu94j3884jdopsl" 242 | :oauth_version "1.0"})))) 243 | (is (= (str "AHW5D+8gO+1qPuoM3+DT6AzGyyXoDl1bJ1ce1c9DWk0EXYvelzycBCmeRi" 244 | "rZGiC3RsFBZw8BwFISZCGim9nddw==") 245 | (sig/sign c 246 | (sig/base-string "POST" 247 | "https://photos.example.net/access_token" 248 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 249 | :oauth_signature_method "RSA-SHA1" 250 | :oauth_timestamp "1191242090" 251 | :oauth_token "hh5s93j4hdidpola" 252 | :oauth_nonce "hsu94j3884jdopsl" 253 | :oauth_verifier "hfdp7dh39dks9884" 254 | :oauth_version "1.0"}) 255 | "hdhd0244k9j7ao03"))))) 256 | 257 | (deftest 258 | #^{:doc "test plaintext signatures"} 259 | plaintext-signature 260 | (let [c {:key "dpf43f3p2l4k3l03" 261 | :secret "kd94hf93k423kf44" 262 | :signature-method :plaintext}] 263 | (is (= "kd94hf93k423kf44&" (sig/sign c 264 | (sig/base-string "POST" 265 | "https://photos.example.net/request_token" 266 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 267 | :oauth_signature_method "PLAINTEXT" 268 | :oauth_timestamp "1191242090" 269 | :oauth_nonce "hsu94j3884jdopsl" 270 | :oauth_version "1.0"})))) 271 | 272 | (is (= "kd94hf93k423kf44&hdhd0244k9j7ao03" (sig/sign c 273 | (sig/base-string "POST" 274 | "https://photos.example.net/access_token" 275 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 276 | :oauth_signature_method "PLAINTEXT" 277 | :oauth_timestamp "1191242090" 278 | :oauth_token "hh5s93j4hdidpola" 279 | :oauth_nonce "hsu94j3884jdopsl" 280 | :oauth_verifier "hfdp7dh39dks9884" 281 | :oauth_version "1.0"}) 282 | "hdhd0244k9j7ao03"))))) 283 | 284 | (deftest 285 | #^{:doc "Test verification of signed request."} 286 | verify 287 | (let [c { :key "dpf43f3p2l4k3l03" 288 | :secret "kd94hf93k423kf44" 289 | :signature-method :hmac-sha1}] 290 | 291 | (is (sig/verify "tR3+Ty81lMeYAr/Fid0kMTYa/WM=" 292 | c 293 | (sig/base-string "GET" 294 | "http://photos.example.net/photos" 295 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 296 | :oauth_token "nnch734d00sl2jdk" 297 | :oauth_signature_method "HMAC-SHA1" 298 | :oauth_timestamp "1191242096" 299 | :oauth_nonce "kllo9940pd9333jh" 300 | :oauth_version "1.0" 301 | :file "vacation.jpg" 302 | :size "original"}) 303 | "pfkkdhi9sl3r4s00")) 304 | 305 | (is (sig/verify "kd94hf93k423kf44&" 306 | (assoc c :signature-method :plaintext) 307 | (sig/base-string "POST" 308 | "https://photos.example.net/request_token" 309 | {:oauth_consumer_key "dpf43f3p2l4k3l03" 310 | :oauth_signature_method "PLAINTEXT" 311 | :oauth_timestamp "1191242090" 312 | :oauth_nonce "hsu94j3884jdopsl" 313 | :oauth_version "1.0"}))))) 314 | 315 | 316 | (deftest 317 | #^{:doc "Test encoding."} 318 | url-encode 319 | (is (= "abcABC123" (sig/url-encode "abcABC123"))) 320 | (is (= "-._~" (sig/url-encode "-._~"))) 321 | (is (= "%25" (sig/url-encode "%"))) 322 | (is (= "%2B" (sig/url-encode "+"))) 323 | (is (= "%20" (sig/url-encode " "))) 324 | (is (= "%26%3D%2A" (sig/url-encode "&=*"))) 325 | (is (= "%0A" (sig/url-encode "\u000A"))) 326 | (is (= "%20" (sig/url-encode "\u0020"))) 327 | (is (= "%7F" (sig/url-encode "\u007F"))) 328 | (is (= "%C2%80" (sig/url-encode "\u0080"))) 329 | (is (= "%E2%9C%88" (sig/url-encode "\u2708"))) 330 | (is (= "%E3%80%81" (sig/url-encode "\u3001")))) 331 | 332 | (deftest 333 | #^{:doc "Test decoding."} 334 | url-decode 335 | (is (= (sig/url-decode "abcABC123") "abcABC123")) 336 | (is (= (sig/url-decode "-._~") "-._~")) 337 | (is (= (sig/url-decode "%25") "%")) 338 | (is (= (sig/url-decode "%2B") "+")) 339 | (is (= (sig/url-decode "%20") " ")) 340 | (is (= (sig/url-decode "%26%3D%2A") "&=*")) 341 | (is (= (sig/url-decode "%0A" ) "\u000A")) 342 | (is (= (sig/url-decode "%20" ) "\u0020")) 343 | (is (= (sig/url-decode "%7F" ) "\u007F")) 344 | (is (= (sig/url-decode "%C2%80" ) "\u0080")) 345 | (is (= (sig/url-decode "%E2%9C%88") "\u2708")) 346 | (is (= (sig/url-decode "%E3%80%81") "\u3001"))) 347 | 348 | (deftest 349 | #^{:doc "url form encode"} 350 | url-form-encode 351 | (is (= (sig/url-form-encode {}) "")) 352 | (is (= (sig/url-form-encode {"hello" "there"}) "hello=there")) 353 | (is (= (sig/url-form-encode (sort {"hello" "there" "name" "Bill" })) "hello=there&name=Bill")) 354 | 355 | (is (= (sig/url-form-encode {:hello "there"}) "hello=there")) 356 | (is (= (sig/url-form-encode (sort {:hello "there" :name "Bill" })) "hello=there&name=Bill")) 357 | 358 | (is (= (sig/url-form-encode {:hello "there"}) "hello=there")) 359 | (is (= (sig/url-form-encode (sort {:hello "there" :name "Bill Smith" })) "hello=there&name=Bill%20Smith"))) 360 | 361 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | - Support request-tokens in oauth_wrap 2 | - link up access token request 3 | - Authorize WS 4 | - Revoke WS 5 | - Nonce checking 6 | --------------------------------------------------------------------------------