add key tests

This commit is contained in:
liquidz 2013-05-03 00:47:25 +09:00
parent 6c5d5f3b88
commit 1fd4ad1569
12 changed files with 214 additions and 81 deletions

View file

@ -2,8 +2,19 @@
A Clojure library for JSON Web Token(JWT)
## Supporting algorithms
* HS256, HS384, HS512
* RS256, RS384, RS512
## Usage
### Leiningen
```
[clj-jwt "0.0.1"]
```
### Generate
```clojure
(ns foo
(:require
@ -16,15 +27,43 @@ A Clojure library for JSON Web Token(JWT)
:exp (plus (now) (days 1))
:nbf (now)})
(def prv-key (rsa-private-key "private.key" "pass phrase"))
; plain JWT
(-> claim jwt to-str)
; HS256 signed JWT
(-> claim jwt (sign :HS256 "key") to-str)
; HMAC256 signed JWT
(-> claim jwt (sign :HS256 "secret") to-str)
; RS256 signed JWT
(let [prv-key (rsa-private-key "foo.pem")]
(-> claim jwt (sign :RS256 prv-key) to-str))
; RSA256 signed JWT
(-> claim jwt (sign :RS256 prv-key) to-str)
```
### Verify
```clojure
(ns foo
(:require
[jwt.core :refer :all]
[jwt.rsa.key :refer [rsa-private-key rsa-public-key]]
[clj-time.core :refer [now plus days]]))
(def claim
{:iss "foo"
:exp (plus (now) (days 1))
:nbf (now)})
(def prv-key (rsa-private-key "private.key" "pass phrase"))
(def pub-key (rsa-public-key "public.key"))
(let [token (-> claim jwt to-str)]
(-> token str->jwt verify))
(let [token (-> claim jwt (sign :HS256 "secret") to-str)]
(-> token str->jwt (verify "secret")))
(let [token (-> claim jwt (sign :RS256 prv-key) to-str)]
(-> token str->jwt (verify pub-key)))
```
## License

View file

@ -1,17 +1,15 @@
(defproject jwt "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
(defproject clj-jwt "0.0.1"
:description "Clojure library for JSON Web Token(JWT)"
:url "https://github.com/liquidz/clj-jwt"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]
[org.clojure/data.json "0.2.2"]
[org.clojure/data.codec "0.1.0"]
[org.bouncycastle/bcprov-jdk15 "1.46"]
[clj-time "0.5.0"]
]
[clj-time "0.5.0"]]
:profiles {:dev {:dependencies [[midje "1.5.1" :exclusions [org.clojure/clojure]]]}}
:plugins [[lein-midje "3.0.0"]]
:plugins [[lein-midje "3.0.0"]]
:main jwt.core
)
:main jwt.core)

View file

@ -3,44 +3,50 @@
[clojure.string :as str])
(:import [java.io ByteArrayInputStream ByteArrayOutputStream]))
(defprotocol ByteArrayInput
(input-stream [this]))
(defprotocol ByteArrayInput (input-stream [this]))
(extend-type String
ByteArrayInput
(input-stream [src] (ByteArrayInputStream. (.getBytes src "UTF-8"))))
(extend-type (Class/forName "[B")
ByteArrayInput
(input-stream [src] (ByteArrayInputStream. src)))
(defn encode [x]
;; Encoder
(defn encode
[x]
(with-open [in (input-stream x)
out (ByteArrayOutputStream.)]
(base64/encoding-transfer in out)
(.toByteArray out)))
(defn encode-str [x & {:keys [charset] :or {charset "UTF-8"}}]
(defn encode-str
[x & {:keys [charset] :or {charset "UTF-8"}}]
(String. (encode x) charset))
(defn decode [x]
;; Decoder
(defn decode
[x]
(with-open [in (input-stream x)
out (ByteArrayOutputStream.)]
(base64/decoding-transfer in out)
(.toByteArray out)))
(defn decode-str [x & {:keys [charset] :or {charset "UTF-8"}}]
(defn decode-str
[x & {:keys [charset] :or {charset "UTF-8"}}]
(String. (decode x) charset))
(defn url-safe-encode-str [x]
;; URL-Safe Encoder
(defn url-safe-encode-str
[x]
(-> (encode-str x)
(str/replace #"\s" "")
(str/replace "=" "")
(str/replace "+" "-")
(str/replace "/" "_")))
(defn url-safe-decode [^String s]
;; URL-Safe Decoder
(defn url-safe-decode
[^String s]
(-> (case (mod (count s) 4)
2 (str s "==")
3 (str s "=")
@ -49,5 +55,6 @@
(str/replace "_" "/")
decode))
(defn url-safe-decode-str [^String s & {:keys [charset] :or {charset "UTF-8"}}]
(defn url-safe-decode-str
[^String s & {:keys [charset] :or {charset "UTF-8"}}]
(String. (url-safe-decode s) charset))

View file

@ -1,7 +1,7 @@
(ns jwt.core
(:require
[jwt.base64 :refer [url-safe-encode-str url-safe-decode url-safe-decode-str]]
[jwt.sign :refer [get-signature-fn get-verify-fn]]
[jwt.base64 :refer [url-safe-encode-str url-safe-decode-str]]
[jwt.sign :refer [get-signature-fn get-verify-fn supported-algorithm?]]
[clj-time.coerce :refer [to-long]]
[clojure.data.json :as json]
[clojure.string :as str]))
@ -20,7 +20,7 @@
; ----------------------------------
(defprotocol JsonWebToken
"Protocol for JsonWebToken"
(init [this] [this claims] "Initialize token")
(init [this claims] "Initialize token")
(encoded-header [this] "Get url-safe base64 encoded header json")
(encoded-claims [this] "Get url-safe base64 encoded claims json")
(to-str [this] "Generate JsonWebToken as string"))
@ -59,7 +59,7 @@
([this key] (sign this DEFAULT_SIGNATURE_ALGORITHM key))
([this alg key]
(let [this* (set-alg this alg)
sign-fn (comp url-safe-encode-str (get-signature-fn alg))
sign-fn (get-signature-fn alg)
data (str (encoded-header this*) "." (encoded-claims this*))]
(assoc this* :signature (sign-fn key data)))))
@ -70,23 +70,20 @@
(cond
(= :none alg) (= "" (:signature this))
(some #(= % alg) [:HS256 :HS384 :HS512])
(let [sign-fn (comp url-safe-encode-str (get-signature-fn alg))
data (str (encoded-header this) "." (encoded-claims this))]
(= (:signature this) (sign-fn key data)))
(some #(= % alg) [:RS256 :RS384 :RS512])
(supported-algorithm? alg)
(let [verify-fn (get-verify-fn alg)
data (str (encoded-header this) "." (encoded-claims this))]
(verify-fn key data (-> this :signature url-safe-decode)))
)))))
(verify-fn key data (:signature this)))
:else (throw (Exception. "Unkown signature")))))))
; =jwt
(defn jwt [claim] (init (->JWT "" "" "") claim))
(defn str->jwt [jwt-string]
; =str->jwt
(defn str->jwt
[jwt-string]
(let [[header claims signature] (str/split jwt-string #"\.")]
(->JWT (encoded-json->map header)
(encoded-json->map claims)
(if signature signature ""))))
(or signature ""))))

View file

@ -20,4 +20,7 @@
(defn rsa-public-key
[& args]
(.getPublic (apply rsa-key args)))
(let [res (apply rsa-key args)]
(if (= org.bouncycastle.jce.provider.JCERSAPublicKey (type res))
res
(.getPublic res))))

View file

@ -1,32 +1,42 @@
(ns jwt.sign)
(ns jwt.sign
(:require
[jwt.base64 :refer [url-safe-encode-str url-safe-decode]]))
(java.security.Security/addProvider
(org.bouncycastle.jce.provider.BouncyCastleProvider.))
; HMAC
(defn hmac-sign
(defn- hmac-sign
"Function to sign data with HMAC algorithm."
[alg key body & {:keys [charset] :or {charset "UTF-8"}}]
(let [hmac-key (javax.crypto.spec.SecretKeySpec. (.getBytes key charset) alg)
hmac (doto (javax.crypto.Mac/getInstance alg)
(.init hmac-key))]
(.doFinal hmac (.getBytes body charset))))
(url-safe-encode-str (.doFinal hmac (.getBytes body charset)))))
(defn- hmac-verify
"Function to verify data and signature with HMAC algorithm."
[alg key body signature & {:keys [charset] :or {charset "UTF-8"}}]
(= signature (hmac-sign alg key body :charset charset)))
; RSA
(defn rsa-sign [alg key body & {:keys [charset] :or {charset "UTF-8"}}]
(defn- rsa-sign
"Function to sign data with RSA algorithm."
[alg key body & {:keys [charset] :or {charset "UTF-8"}}]
(let [sig (doto (java.security.Signature/getInstance alg "BC")
(.initSign key (java.security.SecureRandom.))
(.update (.getBytes body charset)))]
(.sign sig)))
(url-safe-encode-str (.sign sig))))
(defn rsa-verify [alg key body signature & {:keys [charset] :or {charset "UTF-8"}}]
(defn- rsa-verify
"Function to verify data and signature with RSA algorithm."
[alg key body signature & {:keys [charset] :or {charset "UTF-8"}}]
(let [sig (doto (java.security.Signature/getInstance alg "BC")
(.initVerify key)
(.update (.getBytes body charset)))]
;(.verify sig (.getBytes signature charset))
(.verify sig signature)
))
(.verify sig (url-safe-decode signature))))
(def signature-fns
(def ^:private signature-fns
{:HS256 (partial hmac-sign "HmacSHA256")
:HS384 (partial hmac-sign "HmacSHA384")
:HS512 (partial hmac-sign "HmacSHA512")
@ -34,8 +44,11 @@
:RS384 (partial rsa-sign "SHA384withRSA")
:RS512 (partial rsa-sign "SHA512withRSA")})
(def verify-fns
{:RS256 (partial rsa-verify "SHA256withRSA")
(def ^:private verify-fns
{:HS256 (partial hmac-verify "HmacSHA256")
:HS384 (partial hmac-verify "HmacSHA384")
:HS512 (partial hmac-verify "HmacSHA512")
:RS256 (partial rsa-verify "SHA256withRSA")
:RS384 (partial rsa-verify "SHA384withRSA")
:RS512 (partial rsa-verify "SHA512withRSA")})
@ -46,4 +59,4 @@
(def get-signature-fn (partial get-fns signature-fns))
(def get-verify-fn (partial get-fns verify-fns))
(def supported-algorithm? (set (keys verify-fns)))

View file

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcEfIgOOMqMUpSoLGUlmcAdoes
H2i1zeGDf7mlzj/IusPoWdtLtLKoxGId93ijW1rnEDv6FF+DJXoV6zCS6cN6kx8Z
DPg7tRb1oPbgM9tsyS5LdvJI3Tp3xbKHc+kVJMc0lpcpplOoAS4p7SK/76aXCN+S
6G40OWoV10FISOW1/QIDAQAB
-----END PUBLIC KEY-----

View file

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7GCvT//6UKOcM5cZb45CvK9n3
zce71AYTMHMRWh9leARvhGspEHkGHB/5d2TBvqulssNAwT82Wpgemeh995arbm4Y
dcKEg9SbHEKOKt+BQNCHZ53E5YGXSGcKLkJxPnFQT8TDSRZfzh2zs+lw4RO99QVR
vltKEwQ4l6ik9bx8FQIDAQAB
-----END PUBLIC KEY-----

View file

@ -3,7 +3,6 @@
[jwt.base64 :refer :all]
[midje.sweet :refer :all]))
(facts "base64/encode"
(fact "string -> byte array encode"
(class (encode "foo")) => (Class/forName "[B")
@ -37,8 +36,7 @@
(fact "string -> string decode"
(decode-str "Zm9v") => "foo"
(decode-str "YmFy") => "bar"
(decode-str "Zm9vLmJhcg==") => "foo.bar"
)
(decode-str "Zm9vLmJhcg==") => "foo.bar")
(fact "byte array -> string decode"
(decode-str (.getBytes "Zm9v" "UTF-8")) => "foo"
@ -72,5 +70,4 @@
(class (url-safe-decode "Zm9v")) => (Class/forName "[B")
(String. (url-safe-decode "Zm9v")) => "foo"
(String. (url-safe-decode "YmFy")) => "bar"
(String. (url-safe-decode "Zm9vLmJhcg")) => "foo.bar")
)
(String. (url-safe-decode "Zm9vLmJhcg")) => "foo.bar"))

View file

@ -6,9 +6,12 @@
[midje.sweet :refer :all]))
(def claim {:iss "foo"})
(def prv-key (rsa-private-key "test/files/rsa/no_pass.key"))
(def pub-key (rsa-public-key "test/files/rsa/no_pass.key"))
(def dmy-key (rsa-public-key "test/files/rsa/dummy.key"))
(def prv-key (rsa-private-key "test/files/rsa/no_pass.key"))
(def pub-key (rsa-public-key "test/files/rsa/no_pass.pub.key"))
(def enc-prv-key (rsa-private-key "test/files/rsa/3des.key" "pass phrase"))
;(def enc-pub-key (rsa-public-key "test/files/rsa/3des.pub.key" "pass phrase"))
(def enc-pub-key (rsa-public-key "test/files/rsa/3des.pub.key"))
(def dmy-key (rsa-public-key "test/files/rsa/dummy.key"))
(facts "JWT tokenize"
(fact "Plain JWT should be generated."
@ -41,30 +44,49 @@
(-> claim jwt (sign :RS256 prv-key) to-str)
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJmb28ifQ.CqIxuQMw8-sJR0X7v7DqvJua"
"ND2Oy_LpG_kc-SaAM_sfyuC2TMTnqKJiQLmr-VUbM5-EXiCF853xQIr6xnoNmrHFPgbLeynhPyfvsx1u"
"1RIw25z8r0ZJiNtNbSelueYRAjYlrnYUPxqreervGqkLRdEz5uBn3Vy250ggvHb3S_I"))
"1RIw25z8r0ZJiNtNbSelueYRAjYlrnYUPxqreervGqkLRdEz5uBn3Vy250ggvHb3S_I")
(-> claim jwt (sign :RS256 enc-prv-key) to-str)
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJmb28ifQ.0oDxxzP3AoZGosxJSCKE9J9G"
"r_z1TYog8XbQvVaBPAscXPlGpesLP9Nu4PZDwkPa6qMuVYomcea4P61zIeFUNNPq_KhGtOLTEmw7fWX8"
"sjX-YAWxfz1lJFOkCeYrG3rRXzRJStZa-phsfI_f_XyiSbIM6FQrt_pn8z0KJvI-_Oo"))
(fact "RS384 signed JWT should be generated."
(-> claim jwt (sign :RS384 prv-key) to-str)
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJpc3MiOiJmb28ifQ.RIe33rDJ8qKN49qis9DnvEHt"
"cw2af5bndLWaEChSYFRd5MN5e0c936HkyV_40z2DCOLrKt-6HPz1zVePKYOiM0wKr_hEiPEBUtxo4EOS"
"l_XRHgGC2ol3NM57Z0NzUONW4L9GZoojaDopBxfT5zYxt403dgbsp6BzYlnnODHCbfs"))
"l_XRHgGC2ol3NM57Z0NzUONW4L9GZoojaDopBxfT5zYxt403dgbsp6BzYlnnODHCbfs")
(-> claim jwt (sign :RS384 enc-prv-key) to-str)
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJpc3MiOiJmb28ifQ.Goh6VyLf3RmVzfh708QKLrVb"
"x4nLa8SOnXb3mV_UqGRqU6KGO712rMNXbRVddkktC1d1xgxv13XKgZX2RVTMb0utI_yii0hCSHijnvSb"
"x7CgQOJLdXPryQ_K1pUcZWPZbQ965O07XxHApGEjMPVZYFF6GhiGe0xBjR4TCNNOipA"))
(fact "RS512 signed JWT should be generated."
(-> claim jwt (sign :RS512 prv-key) to-str)
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJmb28ifQ.stQhJDu0Hv1ZMTBBYyGNPjap"
"HyDrWRDJSJARhHXmC0T2LkOEtNGBFeQ4mojvxcwd1u2FUu9N5KEmCsmqMpaIubVvdd4GkmGQ4REhR7Cm"
"YFBvB4dFPCpG_B8jn5QkYpXm_zr4wXhW6mdSR4oq_wsULT5O4Z8haoSCl3ysT9SbI0g"))
"YFBvB4dFPCpG_B8jn5QkYpXm_zr4wXhW6mdSR4oq_wsULT5O4Z8haoSCl3ysT9SbI0g")
(let [d (date-time 2000 1 2 3 4 5)
claim (merge claim {:exp (plus d (days 1)) :nbf d})
token (jwt claim)]
(fact "'exp' claim should be converted as IntDate."
(-> token :claims :exp) => 946868645)
(-> claim jwt (sign :RS512 enc-prv-key) to-str)
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJmb28ifQ.LsOapoyhzPzsFy1dkP7o-Se9"
"4VKnwuyigZW5i0r0nTymBHa9auctCW9saJIk7Lt9GVLQkRM92exgEX3rqntZPQXm960FPr5-csgCuHnV"
"qACM0zaoAL3x97lOgK_f93aw2g7Tv3AtXfN9LZ8P7YMfmnwGdeqZRREXWJcpc-7Dioo"))
(fact "'nbf' claim should be converted as IntDate."
(-> token :claims :nbf) => 946782245)))
(fact "'exp', 'nbf', 'iot' claims should be converted as IntDate."
(let [d (date-time 2000 1 2 3 4 5)
claim (merge claim {:exp (plus d (days 1)) :nbf d :iot d :dmy d})
token (jwt claim)]
(-> token :claims :exp) => 946868645
(-> token :claims :nbf) => 946782245
(-> token :claims :iot) => 946782245
(-> token :claims :dmy) => d)))
(facts "JWT verify"
(fact "Unknown signature algorithm should be thrown exception."
(verify (->JWT {:typ "JWT" :alg "DUMMY"} claim "")) => (throws Exception)
(verify (->JWT {:typ "JWT" :alg "DUMMY"} claim "") "") => (throws Exception))
(fact "Plain JWT should be verified."
(-> claim jwt verify) => true
(-> claim jwt to-str str->jwt verify) => true
@ -88,21 +110,32 @@
(fact "RS256 signed JWT should be verified."
(-> claim jwt (sign :RS256 prv-key) (verify pub-key)) => true
(-> claim jwt (sign :RS256 prv-key) to-str str->jwt (verify pub-key)) => true
(-> claim jwt (sign :RS256 prv-key) (verify dmy-key)) => false)
(-> claim jwt (sign :RS256 prv-key) (verify dmy-key)) => false
(-> claim jwt (sign :RS256 enc-prv-key) (verify enc-pub-key)) => true
(-> claim jwt (sign :RS256 enc-prv-key) to-str str->jwt (verify enc-pub-key)) => true
(-> claim jwt (sign :RS256 enc-prv-key) (verify dmy-key)) => false)
(fact "RS384 signed JWT should be verified."
(-> claim jwt (sign :RS384 prv-key) (verify pub-key)) => true
(-> claim jwt (sign :RS384 prv-key) to-str str->jwt (verify pub-key)) => true
(-> claim jwt (sign :RS384 prv-key) (verify dmy-key)) => false
)
(-> claim jwt (sign :RS384 enc-prv-key) (verify enc-pub-key)) => true
(-> claim jwt (sign :RS384 enc-prv-key) to-str str->jwt (verify enc-pub-key)) => true
(-> claim jwt (sign :RS384 enc-prv-key) (verify dmy-key)) => false)
(fact "RS512 signed JWT should be verified."
(-> claim jwt (sign :RS512 prv-key) (verify pub-key)) => true
(-> claim jwt (sign :RS512 prv-key) to-str str->jwt (verify pub-key)) => true
(-> claim jwt (sign :RS512 prv-key) (verify dmy-key)) => false))
(-> claim jwt (sign :RS512 prv-key) (verify dmy-key)) => false
(-> claim jwt (sign :RS512 enc-prv-key) (verify enc-pub-key)) => true
(-> claim jwt (sign :RS512 enc-prv-key) to-str str->jwt (verify enc-pub-key)) => true
(-> claim jwt (sign :RS512 enc-prv-key) (verify dmy-key)) => false))
(facts "str->jwt"
(facts "str->jwt function should work."
(let [before (jwt claim)
after (-> before to-str str->jwt)]
(fact "plain jwt"

36
test/jwt/rsa/key_test.clj Normal file
View file

@ -0,0 +1,36 @@
(ns jwt.rsa.key-test
(:require
[jwt.rsa.key :refer :all]
[midje.sweet :refer :all]))
(facts "rsa private key"
(fact "non encrypt key"
(type (rsa-private-key "test/files/rsa/no_pass.key"))
=> org.bouncycastle.jce.provider.JCERSAPrivateCrtKey)
(fact "crypted key"
(type (rsa-private-key "test/files/rsa/3des.key" "pass phrase"))
=> org.bouncycastle.jce.provider.JCERSAPrivateCrtKey)
(fact "crypted key wrong pass-phrase"
(rsa-private-key "test/files/rsa/3des.key" "wrong pass phrase")
=> (throws org.bouncycastle.openssl.EncryptionException)))
(facts "rsa public key"
(fact "non encrypted key"
(type (rsa-public-key "test/files/rsa/no_pass.key"))
=> org.bouncycastle.jce.provider.JCERSAPublicKey)
(fact "encrypted key"
(type (rsa-public-key "test/files/rsa/3des.key" "pass phrase"))
=> org.bouncycastle.jce.provider.JCERSAPublicKey
)
(fact "encrypted key with wrong pass phrase"
(type (rsa-public-key "test/files/rsa/3des.key" "wrong pass phrase"))
=> (throws org.bouncycastle.openssl.EncryptionException)
)
)

View file

@ -6,8 +6,7 @@
[midje.sweet :refer :all]))
(facts "HMAC"
(let [[hs256 hs384 hs512] (map #(comp url-safe-encode-str (get-signature-fn %))
[:HS256 :HS384 :HS512])
(let [[hs256 hs384 hs512] (map get-signature-fn [:HS256 :HS384 :HS512])
key "foo", body "foo"]
(fact "HS256"
(hs256 key body) => "CLo1fidPUoBldmx3CmOav2gJs5zP03wqMVfH9RlU2go")
@ -20,8 +19,7 @@
"TNH67E1N7Rh_6z9XnIJBCPj2g"))))
(facts "RSA"
(let [[rs256 rs384 rs512] (map #(comp url-safe-encode-str (get-signature-fn %))
[:RS256 :RS384 :RS512])
(let [[rs256 rs384 rs512] (map get-signature-fn [:RS256 :RS384 :RS512])
key (rsa-private-key "test/files/rsa/no_pass.key")
body "foo"]
(fact "RS256"