19 February 2015

Tags: clojure buddy security

Part 3 in my blog series about securing clojure web services using buddy. In this episode we’ll be looking at how we might handle revocation of previously issued auth tokens.

Introduction

Sample code (tagged for each blog post) can be found on github

In part 2 I said that my next post would be about authorization using tokens in a service application. Well my conscience got the better of me and I decided I had to address the slightly thorny issue of how to handle token revocation first. In part 2 I left you in a state where you’d have a really hard time locking a user out or changing access rights. You would have to trust that the user re-authenticated (or change the key-pair for token signing/unsigning).

Opposing forces

Some of the things we are trying to achieve with our auth design are:
  • Avoiding session state for authentication and authorization. Hence the introduction of self contained auth tokens

  • The auth service shouldn’t become a huge dependency magnet, ideally only client facing apps should have to call the auth-service, whilst the service apps would only use the auth-token for authenticating and authorizing requests

  • The user shouldn’t be prompted for his/her credentials more than necessary

The reality though is that:
  • We have to be able to lock down a user (malicious or whatever reason)

  • We should be able to change a users rights without forcing a re-authentication

  • Checking whether a token has been revoked would be impossible without storing state about that fact somewhere

  • Continuously checking with the auth-service whether a token has been revoked and/or rights have changed with the auth service would negate the use of tokens in the first place

Refresh tokens

I briefly started reading up on Oath2 Refresh tokens. It have to admin I didn’t quite get it until I read a farily explanatory post on stackoverflow.

The gist of it that we issue two tokens upon authentication. An authentication token (or access token if you like) and a refresh token. This allows us to set a shorter expiry for the auth token, and we can use the refresh-token to request a new auth token when a previous one has expired. The sole purpose of refresh tokens is to be able to request new auth tokens.

Solution outline

The diagram below (UML with liberties) illustrates how refresh-tokens might work for us.

refresh token
Steps
  1. User logs in with username/password

  2. The web app invokes the create-auth-token service in acme-auth. This in turn

    1. authenticates the user

    2. creates an auth-token

    3. creates a refresh token

  3. The refresh token is stored in a refresh_tokens table

  4. Both the auth-token and refresh-token is returned to the web-app

  5. The web app stores the tokens in a cookie which is returned to the browser

  6. User makes a request (with a valid auth token)

  7. The web app might make a call to a resource server/service app (providing the auth-token as a auth-header in the request)

  8. At some point later after the auth-token has expired (say 30 minutes) the user makes another request

  9. The web app finds that the auth-token has expired and request a new auth-token using the refresh-token (from the cookie)

  10. We retrieve the stored refresh-token to check if it still valid (ie not revoked)

  11. We invalidate the existing refresh token in the db (will explain this bit when we look at the implementation)

  12. We create a new auth token and a new refresh token. The new refresh token is stored in db

  13. A new token-pair is returned to the web-app

  14. The web app can now make a request to a resource server/service with a valid auth-token

  15. Finally the cookie is updated with the new token-pair

Where is the code man ?

Well that was a long intro, so if you are still following along it’s time to have a look at what changes and additions are needed from part 1 and 2.

Changing token creation in acme-auth

(defn- unsign-token [auth-conf token]
  (jws/unsign token (pub-key auth-conf)))

(defn- make-auth-token [auth-conf user]                                        (1)
  (let [exp (-> (t/plus (t/now) (t/minutes 30)) (jws/to-timestamp))]
    (jws/sign {:user (dissoc user :password)}
              (priv-key auth-conf)
              {:alg :rs256 :exp exp})))

(defn- make-refresh-token! [conn auth-conf user]                               (2)
  (let [iat (jws/to-timestamp (t/now))
        token (jws/sign {:user-id (:id user)}
                        (priv-key auth-conf)
                        {:alg :rs256 :iat iat :exp (-> (t/plus (t/now) (t/days 30)) (jws/to-timestamp))})]

    (store/add-refresh-token! conn {:user_id (:id user)                        (3)
                                    :issued iat
                                    :token token})
    token))

(defn make-token-pair! [conn auth-conf user]                                   (4)
  {:token-pair {:auth-token (make-auth-token auth-conf user)
                :refresh-token (make-refresh-token! conn auth-conf user)}})


(defn create-auth-token [ds auth-conf credentials]                             (5)
  (jdbc/with-db-transaction [conn ds]
    (let [[ok? res] (auth-user conn credentials)]
      (if ok?
        [true (make-token-pair! conn auth-conf (:user res))]
        [false res]))))
1 The auth token store user and role info as in part 1, but we now have the option of shortening the expiry
2 For simplicity we have created the refresh token using the same key-pair as for the auth token. The refresh token contains only user-id and issued at time (iat). This allows us retrieval of the db stored token info later on. The expiry for this token can be as long as you are comfortable with (30 days in this instance)
3 We store the token in the refresh_token table with some fields extracted for ease of querying
4 We now return a map with both the auth-token and our shiny new refresh-token
5 The entry point service for token creation

Refreshing tokens

(defn refresh-auth-token [ds auth-conf refresh-token]
  (if-let [unsigned (unsign-token auth-conf refresh-token)]                                               (1)
    (jdbc/with-db-transaction [conn ds]
      (let [db-token-rec (store/find-token-by-unq-key conn (:user-id unsigned) (:iat unsigned))           (2)
            user (store/find-user-by-id conn (:user_id db-token-rec))]
        (if (:valid db-token-rec)                                                                         (3)
          (do
            (store/invalidate-token! conn (:id db-token-rec))                                             (4)
            [true (make-token-pair! conn auth-conf user)])                                                (5)
          [false {:message "Refresh token revoked/deleted or new refresh token already created"}])))
    [false {:message "Invalid or expired refresh token provided"}]))
1 We unsign the refresh-token to ensure it is valid (not tampered with or expired)
2 We use information from the refresh token to retrieve it’s db stored representation.
3 This test could return false for 3 cases; token deleted, token has been revoked or the token has been invalidated because a new refresh token has been created
4 The existing refresh token is invalidated in the database
5 We create a new token pair (where the newly created refresh token is stored in a new db row in the refrest_token table)
Why creating a new refresh token every time ?

Imagine that someone gets hold of a users refresh token. Lets say the user requests a token refresh first, now if the hijacker is making a refresh-request with the hijacked request token we detect that a refresh is attempted on a token that is already invalid. We can’t tell if the user or the hijacker is first, but either way we could take action (trigger warning/lock user account etc) In the code above we can’t tell the diffence between why a refresh token is invalid, so you might wish to have a separate flag for this particular check.

Middleware changes in the acme-webstore

(defn wrap-auth-cookie [handler cookie-secret]                                    (1)
  (-> handler
      (wrap-session
       {:store (cookie-store {:key cookie-secret})
        :cookie-name "acme"
        :cookie-attrs {:max-age (* 60 60 24 30)}}))) ;; you should probably add :secure true to enforce https


(defn unsign-token [token]
  (jws/unsign token (ks/public-key (io/resource "auth_pubkey.pem"))))


(defn wrap-auth-token [handler]                                                  (2)
  (fn [req]
    (let [auth-token (-> req :session :token-pair :auth-token)
          unsigned-auth (when auth-token (unsign-token auth-token))]
      (if unsigned-auth
        (handler (assoc req :auth-user (:user unsigned-auth)))
        (handler req)))))

(defn- handle-token-refresh [handler req refresh-token]
  (let [[ok? res] (refresh-auth-token refresh-token)                             (4)
        user (:user (when ok? (unsign-token (-> res :token-pair :auth-token))))]
    (if user
      (-> (handler (assoc req :auth-user user))                                  (5)
          (assoc :session {:token-pair (:token-pair res)}))
      {:status 302
       :headers {"Location " (str "/login?m=" (:uri req))}})))                   (6)

(defn wrap-authentication [handler]
  (fn [req]
    (if (:auth-user req)
      (handler req)
      (if-let [refresh-token (-> req :session :token-pair :refresh-token)]
        (handle-token-refresh handler req refresh-token)                         (3)
          {:status 302
           :headers {"Location " (str "/login?m=" (:uri req))}}))))
1 The only change we made to the cookie middleware is increase the ttl.
2 The wrap-auth-token middleware just needed to change to handle that auth-token is found as part of a token pair (not shown: the login handler adds the token pair to the session upon successful authentication)
3 If the auth token has expired and refresh token exists we initiate an attempt to refresh the token pair
4 Invokes the acme-auth service for requesting token refresh
5 If a refreshing the token pair was successful we invoke the next handler in the chain and assoc the new token pair with the session key in the response (which in turn ends up in the cookie)
6 We give up, you have to log in again
It might not be a great ideat to store the auth token and the refresh token in the same cookie. Haven’t really thought that bit through tbh.

Summary

A lot of thinking and not a lot of code this time. But I feel we have come up with a solution that might provide a suitable balance between risk and statelessless with regards to revoking tokens/user access. Refresh tokens allows us to stay clear of sessions and avoid asking the usere for their credentials. CSRF is obviously still an issue, but we have taken some small steps to detect when the users cookie might have been hijacked.

The next episode will definately be about authentication and authorization in a service app.

comments powered by Disqus