27 January 2015
Tweet
There is much more to securing web apps and microservices than just authentication and authorization. This blog series will almost exclusively focus on those two aspects. |
Let’s say you have decided to go down the microservices path with Clojure. How would you go about implementing authentication and authorization for your various apps and services ?
In this blog series I’ll take you through my stumblings through how you might address some of the concerns. At this point I have no idea how it might turn out, but I’m pretty sure I’ll learn quite a bit along the way.
To illustrate various aspects I’ll be using the following sample high-level architecture as a starting point. It’s just a sketch, so don’t get too hung up on the dependency arrows, that might change.
You’ll find the evolving code examples at https://github.com/rundis/acme-buddy. A tag will be created for each blog posting.
The most known and used library in clojure for securing your ring webapps is friend. To my knowledge it’s a great library, and you should seriously consider using it for your apps as well.
A little while back I did a small spike on using friend and liberator. Liberator is a super library for rest enabling your applications/services. I came across the blog post API Authentication with Liberator and Friend. I tried to implement something similar but couldn’t quite get it working and I have to admit I had problems groking what was actually going on.
So for this blog series I decided to start off with something less opinionated. Hopefully that will enable me to understand more about the concerns involved. In buddy I found an a la carte menu of building blocks that looked very promising as a starting point.
The goal for this first post is to create a service that allows a caller to authenticate a user by credentials and receive an authentication token upon successful authentication. That token can then be used by services and apps to authenticate and authorize requests for the duration of defined lifespan for the token. The service will be implemented in the acme-auth service app.
For this sample app we’ll use a plain old boring rdbms. The schema will be as follows.
We need to store our passwords securely hashed in the user table. Buddy provides buddy-hashers.
(ns acme-auth.service
(:require [buddy.hashers :as hs]
[acme-auth.store :as store]))
(defn add-user! [ds user]
(store/add-user! ds (update-in user [:password] #(hs/encrypt %))))
hs/encrypt - Hashes the password using bcrypt+sha512 (default, others available). Buddy generates a random salt if you don’t provide one as an option param.
1bcrypt+sha512$232da7e602406d818c6768194$312$4243261243132246c32674a576d514c75585255434f64444f4c59414b653843357a4e645547397279616c304a696f525656393166434862347a2e564b
The password hash we store to the database consists of 4 parts concatinated with $.
Algorithm
Salt - In our case the randomly generated by buddy
Iterations - Number of iterations used for hashing the pwd
Encrypted password hash
(defn auth-user [ds credentials]
(let [user (store/find-user ds (:username credentials))
unauthed [false {:message "Invalid username or password"}]]
(if user
(if (hs/check (:password credentials) (:password user)) (1)
[true {:user (dissoc user :password)}] (2)
unauthed)
unauthed)))
1 | Verify provided plain text password credential against the hashed password in the db |
2 | You probably don’t want to ship the password in the token ! |
Bcrypt is intentionally relatively slow. It’s a measure to help prevent brute force attacks. |
With the user store in place we can turn our attention to creating our (signed) token. Buddy provides us with buddy-sign. We could have opted for a HMAC based algorithm, but we’ll take it up one notch and use an algorithm that requires a public/private key-pair. Not only that, but we’ll serialize our token content in a json format following the jws draft spec.
acme-auth will own the private key and use that for signing whilst the other apps will have the public key for unsigning the token.
You’ll be asked to enter a passphrase in both steps below. Keep it safe !
openssl genrsa -aes128 -out auth_privkey.pem 2048
You should probably use something stronger than -aes128. You’ll need to fiddle with your JVM, but might be worth it unless it’s important for you that your government agencies have access to decrypting your token signatures. |
openssl rsa -pubout -in auth_privkey.pem -out auth_pubkey.pem
(ns acme-auth.service
(:require [buddy.sign.generic :as sign]
[buddy.sign.jws :as jws]
[buddy.core.keys :as ks]
[clj-time.core :as t]
[clojure.java.io :as io]))
(defn- pkey [auth-conf] (1)
(ks/private-key
(io/resource (:privkey auth-conf))
(:passphrase auth-conf)))
(defn create-auth-token [ds auth-conf credentials]
(let [[ok? res] (auth-user ds credentials)
exp (-> (t/plus (t/now) (t/days 1)) (jws/to-timestamp))] (2)
(if ok?
[true {:token (jws/sign res (3)
(pkey auth-conf)
{:alg :rs256 :exp exp})}]
[false res])))
1 | Helper function to read the private key we generated above |
2 | Sets a timestamp for when the token expires |
3 | Creates a signed token |
Base64 encoded string with header data (algorithm and other optional headers you might have set)
Base64 encoded json string with your message (claims in jws speak). Expiry ie. :exp is also a claim btw.
Base64 encoded MAC (Message Authentication Code) signature for our message (header + claims)
With that knowledge in mind, you see why it might be a good idea to leave the password out of the token (even though it would have been the hashed pwd we’re talking about).
(defn create-auth-token [req]
(let [[ok? res] (service/create-auth-token (:datasource req)
(:auth-conf req)
(:params req))]
(if ok?
{:status 201 :body res}
{:status 401 :body res})))
(defroutes app-routes
(POST "/create-auth-token" [] handlers/create-auth-token))
(defn wrap-datasource [handler]
(fn [req]
(handler (assoc req :datasource (get-ds)))))
(defn wrap-config [handler]
(fn [req]
(handler (assoc req :auth-conf {:privkey "auth_privkey.pem"
:passphrase "secret-key"}))))
(def app
(-> app-routes
wrap-datasource
wrap-config
wrap-keyword-params
wrap-json-params
wrap-json-response))
curl -i -X POST -d '{"username": "test", "password":"secret"}' -H "Content-type: application/json" http://localhost:6001/create-auth-token
Would yield something like:
{"token":"eyJ0eXAiOiJKV1MiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjp7InVzZXItcm9sZXMiOlt7InJvbGUtaWQiOjEwLCJhcHBsaWNhdGlvbi1pZCI6MTB9LHsicm9sZS1pZCI6MTEsImFwcGxpY2F0aW9uLWlkIjoxMH1dLCJ1c2VybmFtZSI6InRlc3QiLCJpZCI6MX0sImV4cCI6MTQyMjMxNDk3MH0.bKB3fh2CcPWqP85CK18U_IITxkRce8Xuj8fZGvhqjAaq1dWeiDMKOAGfSlg6GGJi-CrRepMaLOEfAVN23R7yoYb543wgm1Tv_pOYuNQ02tYRQMRJXSxVKS1m9zMEWlszLVet8Q3kfrLBaOxjdvjSp8exjsPeOcfCaqdcXPn9mwWSz0X8k1iaLbnY2fRL0mWbbG8rz4bSUSE0KX0xnKH3LqrtJcZE3BDHSr7tVqaxcHaFt4ivRpk3EYBzMtwRSCQ4jwAMibsh1XhvJMo4QeDwil-et70qJMV5XCJOsAr3SF4FVlNeUsNx2Aj1lORGIN7c8xKq-MDaTaGYV2O7L_0mGA"}
Unsigning the token is quite similar to the signing. However when unsigning you must have the public key we generated earlier.
For the token above, the claims part of the message would look like this:
{"user":{"user-roles":[{"role-id":10,"application-id":10},
{"role-id":11,"application-id":10}],
"username":"test",
"id":1},
"exp":1422314970}
We have created a small clojure app with a user database and a rest service that authenticates a user and returns a signed token with information about the user and his/her role+app/service authorizations. We’ve briefly covered password hashing and message signing using buddy.
The auth-token service will serve as a building block for the next step. How do we make use of token for authentication and authorization purposes in the acme-webstore ? That’s the topic of my next blog post in this series. Stay tuned !