Skip to content

Commit

Permalink
redis store works with clients, tokens and users
Browse files Browse the repository at this point in the history
  • Loading branch information
pelle committed Mar 20, 2012
1 parent 8d735cc commit ba7a6f3
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 69 deletions.
25 changes: 22 additions & 3 deletions README.md
Expand Up @@ -74,16 +74,36 @@ Stores are used to store tokens and will be used to store clients and users as w

There is a generalized protocol called Store and currently a simple memory implementation used for it.

It should be pretty simple to implement this Store with redis, sql, datomic or what have you. I will write a reference implementation using redis next.
It should be pretty simple to implement this Store with redis, sql, datomic or what have you.

The stores used by the various parts are defined in an atom for each type. reset! each of them with your own implementation.
It includes a simple Redis implementation.

The stores used by the various parts are defined in an atom for each type. reset! each of them with your own implementation.

The following stores are currently defined:

* token-store is in clauth.token/token-store
* client-store is in clauth.client/client-store
* user-store is in clauth.user/user-store

To use the redis store add the following to your code:

(reset! token-store (create-redis-store "tokens"))
(reset! client-store (create-redis-store "clients"))
(reset! user-store (create-redis-store "users"))

And wrap your handler with a redis connection middleware similar to this:

(defn wrap-redis-store [app]
(fn [req]
(redis/with-server
{:host "127.0.0.1"
:port 6379
:db 14
}
(app req))))


## Run Demo App

A mini server demo is available. It creates a client for you and prints out instructions on how to issue tokens with curl.
Expand All @@ -94,7 +114,6 @@ A mini server demo is available. It creates a client for you and prints out inst

The goal is to implement the full [OAuth2 spec](http://tools.ietf.org/html/draft-ietf-oauth-v2-25) in this order:

* Redis Store implementation
* [Authorization Code Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.1)
* [Implicit Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-4.2)
* [Refresh Tokens](http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-1.5)
Expand Down
228 changes: 188 additions & 40 deletions docs/uberdoc.html

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion src/clauth/client.clj
Expand Up @@ -10,6 +10,12 @@

(defn client-app
"Create new client-application record"
([attrs] ; Swiss army constructor. There must be a better way.
(cond
(nil? attrs) nil
(instance? ClientApplication attrs) attrs
(instance? java.lang.String attrs) (client-app (cheshire.core/parse-string attrs true))
:default (ClientApplication. (attrs :client-id) (attrs :client-secret) (attrs :name) (attrs :url))))
([] (client-app nil nil))
([name url] (ClientApplication. (generate-token) (generate-token) name url)))

Expand All @@ -21,7 +27,7 @@
(defn fetch-client
"Find OAuth token based on the token string"
[t]
(fetch @client-store t))
(client-app (fetch @client-store t)))

(defn store-client
"Store the given ClientApplication and return it."
Expand Down
46 changes: 34 additions & 12 deletions src/clauth/demo.clj
Expand Up @@ -3,6 +3,8 @@
(:use [clauth.endpoints])
(:use [clauth.client])
(:use [clauth.token])
(:use [clauth.store.redis])
(:require [redis.core :as redis])
(:use [ring.adapter.jetty])
(:use [ring.middleware.cookies])
(:use [ring.middleware.params]))
Expand All @@ -23,22 +25,42 @@
((token-handler) req )
((require-bearer-token! handler) req)))

(defn wrap-redis-store [app]
(fn [req]
(redis/with-server
{:host "127.0.0.1"
:port 6379
:db 14
}
(app req))))

(defn -main
"start web server. This first wraps the request in the cookies and params middleware, then requires a bearer token.
The function passed in this example to require-bearer-token is a clojure set containing the single value \"secret\".
You could instead use a hash for a simple in memory token database or a function querying a database."
[]
(let [client (register-client)]
(println "App starting up:")
(prn client)
(println "Token endpoint /token")
(println)
(println "Fetch a Client Credentialed access token:")
(println)
(println "curl http://127.0.0.1:3000/token -d grant_type=client_credentials -u " (clojure.string/join ":" [(:client-id client) (:client-secret client)]) )
(println)
(run-jetty (-> routes
(wrap-params)
(wrap-cookies)) {:port 3000})))
(do

(reset! token-store (create-redis-store "tokens"))
(reset! client-store (create-redis-store "clients"))
(redis/with-server
{:host "127.0.0.1"
:port 6379
:db 14
}
(let [client ( or (first (clients)) (register-client))]
(println "App starting up:")
(prn client)
(println "Token endpoint /token")
(println)
(println "Fetch a Client Credentialed access token:")
(println)
(println "curl http://127.0.0.1:3000/token -d grant_type=client_credentials -u " (clojure.string/join ":" [(:client-id client) (:client-secret client)]) )
(println)

(run-jetty (-> routes
(wrap-params)
(wrap-cookies)
(wrap-redis-store)) {:port 3000})))))
2 changes: 1 addition & 1 deletion src/clauth/middleware.clj
@@ -1,5 +1,5 @@
(ns clauth.middleware
(use [clauth.token]))
(:use [clauth.token]))

(defn wrap-bearer-token
"Wrap request with a OAuth2 bearer token as defined in http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08.
Expand Down
7 changes: 3 additions & 4 deletions src/clauth/store/redis.clj
Expand Up @@ -12,9 +12,8 @@
(defn all-in-namespace
"get all items in namespace"
[namespace]
(let [ks (remove nil? (namespaced-keys namespace))
gt `(redis.core/mget ~@ks)]
(if (not-empty ks) (eval gt)))) ; This seems wrong to me. Is this the best way of doing it?
(let [ks (remove nil? (namespaced-keys namespace))]
(if (not-empty ks) (apply redis/mget ks))))


(defrecord RedisStore [namespace]
Expand All @@ -24,7 +23,7 @@

(store [this key_param item]
(do
(redis/set (str namespace "/" (item key_param)) (cheshire.core/generate-string item))
(redis/set (str namespace "/" (key_param item)) (cheshire.core/generate-string item))
item)
)
(entries [this] (map #( cheshire.core/parse-string % true) (all-in-namespace namespace) ))
Expand Down
16 changes: 10 additions & 6 deletions src/clauth/token.clj
@@ -1,7 +1,8 @@
(ns clauth.token
(:use [clauth.store])
(:require [crypto.random])
(:require [clj-time.core :as time]))
(:require [clj-time.core :as time])
(:require [cheshire.core]))

(defprotocol Expirable
"Check if object is valid"
Expand Down Expand Up @@ -34,9 +35,12 @@
* scope - An optional vector of scopes authorized
* object - An optional object authorized. Eg. account, photo"

([attrs]
(OAuthToken. (attrs "token") (attrs "client") (attrs "subject") (attrs "expires") (attrs "scope") (attrs "object"))
)
([attrs] ; Swiss army constructor. There must be a better way.
(cond
(nil? attrs) nil
(instance? OAuthToken attrs) attrs
(instance? java.lang.String attrs) (oauth-token (cheshire.core/parse-string attrs true))
:default (OAuthToken. (attrs :token) (attrs :client) (attrs :subject) (attrs :expires) (attrs :scope) (attrs :object))))
([client subject]
(oauth-token client subject nil nil nil)
)
Expand All @@ -58,7 +62,7 @@
(defn fetch-token
"Find OAuth token based on the token string"
[t]
(fetch @token-store t))
(oauth-token (fetch @token-store t)))

(defn store-token
"Store the given OAuthToken and return it."
Expand All @@ -68,7 +72,7 @@
(defn tokens
"Sequence of tokens"
[]
(entries @token-store))
(map oauth-token (entries @token-store)))

(defn create-token
"create a unique token and store it in the token store"
Expand Down
9 changes: 8 additions & 1 deletion src/clauth/user.clj
Expand Up @@ -20,6 +20,13 @@

(defn new-user
"Create new user record"
([attrs] ; Swiss army constructor. There must be a better way.
(cond
(nil? attrs) nil
(instance? User attrs) attrs
(instance? java.lang.String attrs) (new-user (cheshire.core/parse-string attrs true))
:default (User. (attrs :login) (attrs :password) (attrs :name) (attrs :url))))

([ login password ] (new-user login password nil nil))
([ login password name url ] (User. login (bcrypt password) name url)))

Expand All @@ -31,7 +38,7 @@
(defn fetch-user
"Find user based on login"
[t]
(fetch @user-store t))
(new-user (fetch @user-store t)))

(defn store-user
"Store the given User and return it."
Expand Down
60 changes: 59 additions & 1 deletion test/clauth/test/store/redis.clj
@@ -1,5 +1,8 @@
(ns clauth.test.store.redis
(:use [clauth.store])
(:use [clauth.token])
(:use [clauth.client])
(:use [clauth.user])
(:use [clauth.store.redis])
(:require [redis.core :as redis])
(:use [clojure.test])
Expand Down Expand Up @@ -29,4 +32,59 @@
(is (= [] (entries st)))
(is (nil? (fetch st "item"))))))))



(deftest token-store-implementation
(redis/with-server
{:host "127.0.0.1"
:port 6379
:db 15
}
(reset! token-store (create-redis-store "tokens"))
(reset-token-store!)
(is (= 0 (count (tokens))) "starts out empty")
(let
[record (oauth-token "my-client" "my-user")]
(is (nil? (fetch-token (:token record))))
(do
(store-token record)
(is (= record (fetch-token (:token record))))
(is (= 1 (count (tokens))) "added one"))))
(reset! token-store (create-memory-store)))

(deftest client-store-implementation
(redis/with-server
{:host "127.0.0.1"
:port 6379
:db 15
}
(reset! client-store (create-redis-store "clients"))
(reset-client-store!)
(is (= 0 (count (clients))) "starts out empty")
(let
[ record (client-app)]
(is (nil? (fetch-client (:client-id record))))
(do
(store-client record)
(is (= record (fetch-client (:client-id record))))
(is (= 1 (count (clients))) "added one"))))
(reset! client-store (create-memory-store)))


(deftest user-store-implementation
(redis/with-server
{:host "127.0.0.1"
:port 6379
:db 15
}
(reset! user-store (create-redis-store "users"))
(reset-user-store!)
(is (= 0 (count (users))) "starts out empty")
(let
[ record (new-user "john@example.com" "password")]
(is (nil? (fetch-user "john@example.com")))
(do
(store-user record)
(is (= record (fetch-user "john@example.com")))
(is (= 1 (count (users))) "added one"))))
(reset! user-store (create-memory-store)))

0 comments on commit ba7a6f3

Please sign in to comment.