first commit
This commit is contained in:
commit
3a11c2cff5
11 changed files with 387 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
/target
|
||||
/lib
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
.lein-deps-sum
|
||||
.lein-failures
|
||||
.lein-plugins
|
||||
.lein-repl-history
|
34
README.md
Normal file
34
README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# clj-jwt
|
||||
|
||||
A Clojure library for JSON Web Token(JWT)
|
||||
|
||||
## Usage
|
||||
|
||||
```clojure
|
||||
(ns foo
|
||||
(:require
|
||||
[jwt.core :refer :all]
|
||||
[jwt.rsa.key :refer [rsa-private-key]]
|
||||
[clj-time.core :refer [now plus days]]))
|
||||
|
||||
(def claim
|
||||
{:iss "foo"
|
||||
:exp (plus (now) (days 1))
|
||||
:nbf (now)})
|
||||
|
||||
; plain JWT
|
||||
(-> claim jwt to-str)
|
||||
|
||||
; HS256 signed JWT
|
||||
(-> claim jwt (sign :HS256 "key") to-str)
|
||||
|
||||
; RS256 signed JWT
|
||||
(let [prv-key (rsa-private-key "foo.pem")]
|
||||
(-> claim jwt (sign :RS256 prv-key) to-str))
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2013 [uochan](http://twitter.com/uochan)
|
||||
|
||||
Distributed under the Eclipse Public License, the same as Clojure.
|
17
project.clj
Normal file
17
project.clj
Normal file
|
@ -0,0 +1,17 @@
|
|||
(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"}
|
||||
: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"]
|
||||
]
|
||||
|
||||
:profiles {:dev {:dependencies [[midje "1.5.1" :exclusions [org.clojure/clojure]]]}}
|
||||
:plugins [[lein-midje "3.0.0"]]
|
||||
|
||||
:main jwt.core
|
||||
)
|
47
src/jwt/base64.clj
Normal file
47
src/jwt/base64.clj
Normal file
|
@ -0,0 +1,47 @@
|
|||
(ns jwt.base64
|
||||
(:require [clojure.data.codec.base64 :as base64]
|
||||
[clojure.string :as str]
|
||||
)
|
||||
(:import [java.io ByteArrayInputStream ByteArrayOutputStream]
|
||||
))
|
||||
|
||||
(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]
|
||||
(with-open [in (input-stream x)
|
||||
out (ByteArrayOutputStream.)]
|
||||
(base64/encoding-transfer in out)
|
||||
(.toString out)))
|
||||
|
||||
(defn url-safe-encode [s]
|
||||
(-> (encode s)
|
||||
(str/replace #"\s" "")
|
||||
(str/replace "=" "")
|
||||
(str/replace "+" "-")
|
||||
(str/replace "/" "_")))
|
||||
|
||||
(defn decode [x]
|
||||
(with-open [in (input-stream x)
|
||||
out (ByteArrayOutputStream.)]
|
||||
(base64/decoding-transfer in out)
|
||||
(.toString out)))
|
||||
|
||||
(defn url-safe-decode [^String s]
|
||||
(-> (case (mod (count s) 4)
|
||||
2 (str s "==")
|
||||
3 (str s "=")
|
||||
s)
|
||||
(str/replace "-" "+")
|
||||
(str/replace "_" "/")
|
||||
decode))
|
||||
|
68
src/jwt/core.clj
Normal file
68
src/jwt/core.clj
Normal file
|
@ -0,0 +1,68 @@
|
|||
(ns jwt.core
|
||||
(:require
|
||||
[jwt.base64 :refer [url-safe-encode]]
|
||||
[jwt.sign :refer [get-signature-fn]]
|
||||
[clj-time.coerce :refer [to-long]]
|
||||
[clojure.data.json :as json]))
|
||||
|
||||
(def ^:private DEFAULT_SIGNATURE_ALGORITHM :HS256)
|
||||
(def ^:private make-encoded-json (comp url-safe-encode json/write-str))
|
||||
(defn- update-map [m k f] (if (contains? m k) (update-in m [k] f) m))
|
||||
(defn- joda-time? [x] (= org.joda.time.DateTime (type x)))
|
||||
(defn- to-intdate [d] {:pre [(joda-time? d)]} (int (/ (to-long d) 1000)))
|
||||
|
||||
(defrecord JWT [header claims signature])
|
||||
|
||||
; ----------------------------------
|
||||
; JsonWebToken
|
||||
; ----------------------------------
|
||||
(defprotocol JsonWebToken
|
||||
"Protocol for JsonWebToken"
|
||||
(init [this] [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"))
|
||||
|
||||
(extend-protocol JsonWebToken
|
||||
JWT
|
||||
(init [this claims]
|
||||
(assoc this :header {:alg "none" :typ "JWT"} :claims claims :signature ""))
|
||||
|
||||
(encoded-header [this]
|
||||
(-> this :header make-encoded-json))
|
||||
|
||||
(encoded-claims [this]
|
||||
(-> this :claims make-encoded-json))
|
||||
|
||||
(to-str [this]
|
||||
(str (encoded-header this) "." (encoded-claims this) "." (get this :signature ""))))
|
||||
|
||||
|
||||
; ----------------------------------
|
||||
; JsonWebSignature
|
||||
; ----------------------------------
|
||||
(defprotocol JsonWebSignature
|
||||
"Protocol for JonWebSignature"
|
||||
(set-alg [this alg] "Set algorithm name to JWS Header Parameter")
|
||||
(sign [this key] [this alg key] "Set signature to this token"))
|
||||
|
||||
(extend-protocol JsonWebSignature
|
||||
JWT
|
||||
(set-alg [this alg]
|
||||
(assoc-in this [:header :alg] (name alg)))
|
||||
|
||||
(sign
|
||||
([this key] (sign this DEFAULT_SIGNATURE_ALGORITHM key))
|
||||
([this alg key]
|
||||
(let [this* (set-alg this alg)
|
||||
sign-fn (comp url-safe-encode (get-signature-fn alg))
|
||||
data (str (encoded-header this*) "." (encoded-claims this*))]
|
||||
(assoc this* :signature (sign-fn key data))))))
|
||||
|
||||
|
||||
; =jwt
|
||||
(defn jwt [claim]
|
||||
(init (->JWT "" "" "")
|
||||
(reduce #(update-map % %2 to-intdate) claim [:exp :nbf :iot])))
|
||||
|
||||
|
15
src/jwt/rsa/key.clj
Normal file
15
src/jwt/rsa/key.clj
Normal file
|
@ -0,0 +1,15 @@
|
|||
(ns jwt.rsa.key
|
||||
(:require [clojure.java.io :as io]))
|
||||
|
||||
(defn- rsa-key [filename]
|
||||
(-> (io/reader filename)
|
||||
org.bouncycastle.openssl.PEMReader.
|
||||
.readObject))
|
||||
|
||||
(defn rsa-private-key
|
||||
[filename]
|
||||
(-> filename rsa-key .getPrivate))
|
||||
|
||||
(defn rsa-public-key
|
||||
[filename]
|
||||
(-> filename rsa-key .getPublic))
|
33
src/jwt/sign.clj
Normal file
33
src/jwt/sign.clj
Normal file
|
@ -0,0 +1,33 @@
|
|||
(ns jwt.sign)
|
||||
|
||||
(java.security.Security/addProvider
|
||||
(org.bouncycastle.jce.provider.BouncyCastleProvider.))
|
||||
|
||||
; HMAC
|
||||
(defn hmac-sha
|
||||
[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))))
|
||||
|
||||
; RSA
|
||||
(defn rsa-sha [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)))
|
||||
|
||||
(def signature-fns
|
||||
{:HS256 (partial hmac-sha "HmacSHA256")
|
||||
:HS384 (partial hmac-sha "HmacSHA384")
|
||||
:HS512 (partial hmac-sha "HmacSHA512")
|
||||
:RS256 (partial rsa-sha "SHA256withRSA")
|
||||
:RS384 (partial rsa-sha "SHA384withRSA")
|
||||
:RS512 (partial rsa-sha "SHA512withRSA")})
|
||||
|
||||
(defn get-signature-fn [alg]
|
||||
(if-let [f (get signature-fns alg)]
|
||||
f
|
||||
(throw (Exception. "Unkown signature"))))
|
||||
|
15
test/files/rsa/rsa.pem
Normal file
15
test/files/rsa/rsa.pem
Normal file
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQC7GCvT//6UKOcM5cZb45CvK9n3zce71AYTMHMRWh9leARvhGsp
|
||||
EHkGHB/5d2TBvqulssNAwT82Wpgemeh995arbm4YdcKEg9SbHEKOKt+BQNCHZ53E
|
||||
5YGXSGcKLkJxPnFQT8TDSRZfzh2zs+lw4RO99QVRvltKEwQ4l6ik9bx8FQIDAQAB
|
||||
AoGAGQLFMSUCqlnBcWbyGmyUdeZd0BOxRLm2SjBq4YHzuoPNy/6euLGcDCYMXDQK
|
||||
wx+zIIaCNZDf22tG9KhMfTJw8KVhFkMQJ9A8FHC53uyPjB8/vnq8169rqEcTRHV+
|
||||
GncnCb4NuBcpDkAe/OSfKTEnMEg4+pGpvblZcVtJvQpVcgECQQDdgVb3ZRW2VJ9O
|
||||
FwwVAjW9/uVOiqxpuLahHjzllWqKYwoWW8otwNQ3dTu2mbPkOXi2rpFPTyBTTJAW
|
||||
Iv71LPaBAkEA2Dr9RjDvCiokv7Je3LmJ2BaDfXdtGSBmyG8Brt9YTve4H2tsYoLE
|
||||
NmYqzPM7W1cd16aWg1FCDsD1evlSi+2DlQJABYr/CiHVcUKc2e9ptfzgK2j9hAGk
|
||||
XuDocQ+4pmYezGe+EOErJgn1RY4BeIhQIB3wD2I+8KUiQfNgh61IhAokAQJBAMy9
|
||||
9mpLFVyzkP5uwAISMOKKVtErjwMWuhwZeCeEVdLYHuCpUARrO60iym4r9c1ETP6Q
|
||||
P75x57GephJeF/pk2I0CQF044ph/kQYk022+4QBIzOQLJ7b1+OREIDRfNpt2FE+0
|
||||
O0RE2fIS3ca8Ppd5ZeMIFCcnWmkrjuPJZUTf7pgdrvY=
|
||||
-----END RSA PRIVATE KEY-----
|
38
test/jwt/base64_test.clj
Normal file
38
test/jwt/base64_test.clj
Normal file
|
@ -0,0 +1,38 @@
|
|||
(ns jwt.base64-test
|
||||
(:require
|
||||
[jwt.base64 :as base64]
|
||||
[midje.sweet :refer :all]))
|
||||
|
||||
|
||||
(facts "base64/encode"
|
||||
(fact "encode from string"
|
||||
(base64/encode "foo") => "Zm9v"
|
||||
(base64/encode "bar") => "YmFy"
|
||||
(base64/encode "foo.bar") => "Zm9vLmJhcg==")
|
||||
|
||||
(fact "encode from byte array"
|
||||
(base64/encode (.getBytes "foo" "UTF-8")) => "Zm9v"
|
||||
(base64/encode (.getBytes "bar" "UTF-8")) => "YmFy"
|
||||
(base64/encode (.getBytes "foo.bar" "UTF-8")) => "Zm9vLmJhcg=="))
|
||||
|
||||
(facts "base64/url-safe-encode"
|
||||
(fact "encode from string"
|
||||
(base64/url-safe-encode "foo") => "Zm9v"
|
||||
(base64/url-safe-encode "bar") => "YmFy"
|
||||
(base64/url-safe-encode "foo.bar") => "Zm9vLmJhcg")
|
||||
|
||||
(fact "encode from byte array"
|
||||
(base64/url-safe-encode (.getBytes "foo" "UTF-8")) => "Zm9v"
|
||||
(base64/url-safe-encode (.getBytes "bar" "UTF-8")) => "YmFy"
|
||||
(base64/url-safe-encode (.getBytes "foo.bar" "UTF-8")) => "Zm9vLmJhcg"))
|
||||
|
||||
|
||||
(fact "base64/decode"
|
||||
(base64/decode "Zm9v") => "foo"
|
||||
(base64/decode "YmFy") => "bar"
|
||||
(base64/decode "Zm9vLmJhcg==") => "foo.bar")
|
||||
|
||||
(fact "base64/url-safe-decode"
|
||||
(base64/url-safe-decode "Zm9v") => "foo"
|
||||
(base64/url-safe-decode "YmFy") => "bar"
|
||||
(base64/url-safe-decode "Zm9vLmJhcg") => "foo.bar")
|
63
test/jwt/core_test.clj
Normal file
63
test/jwt/core_test.clj
Normal file
|
@ -0,0 +1,63 @@
|
|||
(ns jwt.core-test
|
||||
(:require
|
||||
[jwt.core :refer :all]
|
||||
[jwt.rsa.key :refer [rsa-private-key]]
|
||||
[clj-time.core :refer [date-time plus days now]]
|
||||
[midje.sweet :refer :all]))
|
||||
|
||||
(facts "jwt function works fine."
|
||||
(let [claim {:iss "foo"}]
|
||||
(fact "Plain JWT should be generated."
|
||||
(-> claim jwt to-str)
|
||||
=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJmb28ifQ.")
|
||||
|
||||
(fact "If unknown algorithm is specified, exception is throwed."
|
||||
(-> claim jwt (sign :DUMMY "foo")) => (throws Exception))
|
||||
|
||||
(fact "HS256 signed JWT should be generated."
|
||||
(-> claim jwt (sign "foo") to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmb28ifQ.mScNySwrJjjVjGiIaSW0blyb"
|
||||
"g2knXpuokTzYio5XUFg")
|
||||
|
||||
(-> claim jwt (sign :HS256 "foo") to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmb28ifQ.mScNySwrJjjVjGiIaSW0blyb"
|
||||
"g2knXpuokTzYio5XUFg"))
|
||||
|
||||
(fact "HS384 signed JWT should be generated."
|
||||
(-> claim jwt (sign :HS384 "foo") to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJmb28ifQ.DamQl6Dv8-Ya92kJx6zKlF4n"
|
||||
"xX12NO0V0vhFsOGbwTUIdtkc08Rt4pNQZukIJyNc"))
|
||||
|
||||
(fact "HS512 signed JWT should be generated."
|
||||
(-> claim jwt (sign :HS512 "foo") to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJmb28ifQ.RoOaMBk4uzRmCAOCLg5h9QkL"
|
||||
"FAugLwYeGGhXSjlJ57n4EHoapm6nvheJzIF8OlLYtjwdPcdFbsuaTgPSIa1tCQ"))
|
||||
|
||||
(let [key (rsa-private-key "test/files/rsa/rsa.pem")]
|
||||
(fact "RS256 signed JWT should be generated."
|
||||
(-> claim jwt (sign :RS256 key) to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJmb28ifQ.CqIxuQMw8-sJR0X7v7DqvJua"
|
||||
"ND2Oy_LpG_kc-SaAM_sfyuC2TMTnqKJiQLmr-VUbM5-EXiCF853xQIr6xnoNmrHFPgbLeynhPyfvsx1u"
|
||||
"1RIw25z8r0ZJiNtNbSelueYRAjYlrnYUPxqreervGqkLRdEz5uBn3Vy250ggvHb3S_I"))
|
||||
|
||||
(fact "RS384 signed JWT should be generated."
|
||||
(-> claim jwt (sign :RS384 key) to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJpc3MiOiJmb28ifQ.RIe33rDJ8qKN49qis9DnvEHt"
|
||||
"cw2af5bndLWaEChSYFRd5MN5e0c936HkyV_40z2DCOLrKt-6HPz1zVePKYOiM0wKr_hEiPEBUtxo4EOS"
|
||||
"l_XRHgGC2ol3NM57Z0NzUONW4L9GZoojaDopBxfT5zYxt403dgbsp6BzYlnnODHCbfs"))
|
||||
|
||||
(fact "RS512 signed JWT should be generated."
|
||||
(-> claim jwt (sign :RS512 key) to-str)
|
||||
=> (str "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJmb28ifQ.stQhJDu0Hv1ZMTBBYyGNPjap"
|
||||
"HyDrWRDJSJARhHXmC0T2LkOEtNGBFeQ4mojvxcwd1u2FUu9N5KEmCsmqMpaIubVvdd4GkmGQ4REhR7Cm"
|
||||
"YFBvB4dFPCpG_B8jn5QkYpXm_zr4wXhW6mdSR4oq_wsULT5O4Z8haoSCl3ysT9SbI0g"))))
|
||||
|
||||
(let [d (date-time 2000 1 2 3 4 5)
|
||||
claim {:iss "foo" :exp (plus d (days 1)) :nbf d}
|
||||
token (jwt claim)]
|
||||
(fact "'exp' claim should be converted as IntDate."
|
||||
(-> token :claims :exp) => 946868645)
|
||||
|
||||
(fact "'nbf' claim should be converted as IntDate."
|
||||
(-> token :claims :nbf) => 946782245)))
|
||||
|
45
test/jwt/sign_test.clj
Normal file
45
test/jwt/sign_test.clj
Normal file
|
@ -0,0 +1,45 @@
|
|||
(ns jwt.sign-test
|
||||
(:require
|
||||
[jwt.sign :refer :all]
|
||||
[jwt.base64 :refer [url-safe-encode]]
|
||||
[jwt.rsa.key :refer [rsa-private-key]]
|
||||
[midje.sweet :refer :all]))
|
||||
|
||||
(facts "HMAC"
|
||||
(let [[hs256 hs384 hs512] (map #(comp url-safe-encode (get-signature-fn %))
|
||||
[:HS256 :HS384 :HS512])
|
||||
key "foo", body "foo"]
|
||||
(fact "HS256"
|
||||
(hs256 key body) => "CLo1fidPUoBldmx3CmOav2gJs5zP03wqMVfH9RlU2go")
|
||||
|
||||
(fact "HS384"
|
||||
(hs384 key body) => (str "piXjQSLhU8VQMR__GcK-j0-B52Y3YhDbUAqkjRZ5skHGnO8bfaqF9smvE8n-6"
|
||||
"AOR"))
|
||||
(fact "HS512"
|
||||
(hs512 key body) => (str "zpfRr559UfVU-WtKizOGdX7fF46Z0Tburo2T_0CzrEVsGD_JZX0eky96QYdkr"
|
||||
"TNH67E1N7Rh_6z9XnIJBCPj2g"))))
|
||||
|
||||
(facts "RSA"
|
||||
(let [[rs256 rs384 rs512] (map #(comp url-safe-encode (get-signature-fn %))
|
||||
[:RS256 :RS384 :RS512])
|
||||
key (rsa-private-key "test/files/rsa/rsa.pem")
|
||||
body "foo"]
|
||||
(fact "RS256"
|
||||
(rs256 key body) => (str "VUbrxVb4ud4Iqh8h3rBHijagwFbXyml6FkqgYl9JhauWMZReM4brJh__KlBeF"
|
||||
"R30ZruV2_VUpFYEuSnsoO1KrscnZklUow_Z8AKWCrCSxWO1I8qyskbWyN3MBq"
|
||||
"fQxVNEc62xrzMMpdnLq6OpIk--Sh5ZdUYl-tT3wy4HV_sxQUU"))
|
||||
|
||||
(fact "RS384"
|
||||
(rs384 key body) => (str "F1HhYSk8cFdnr1ODDv-Q6YvTpMq3p8STD3lh6gingp1U5gpYmnbMqgOr_YM5z"
|
||||
"jeUsFI1d1FolwfaeKeBRxVo9tjawb-TxFAFIdVLfZpwb3kR7nHq9NsQHfkDf_"
|
||||
"DnfSPOi8d7wX8Eunb-padnM9sn1L4g1GYH9ReuoYhV8JUsJZE"))
|
||||
|
||||
(fact "RS512"
|
||||
(rs512 key body) => (str "VVfaoXP5WUGNSggUE1FVYV-JKZRGnFkm2ATFm2MQ7bZbyan4EBzVPUN1B5Be3"
|
||||
"A-Z1j3LeLKFWhryRRAjzW--Ut5rs5t0MjJ4OgUUhXAEXXAeJfbeEVxzBv4C-F"
|
||||
"e9avjnNjUgcPlJgQAMQbrLirSo8Z8hb1Iqz9f7pUuNLTkAQJA"))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue