first commit

This commit is contained in:
liquidz 2013-04-29 22:24:41 +09:00
commit 3a11c2cff5
11 changed files with 387 additions and 0 deletions

12
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"))))