Tuesday, August 10, 2010

Securing Clojure Web Applications with Sandbar - Part 1

Authorization in the model


This is the first in a series of posts that will show how to use Sandbar to secure web applications. Sandbar is a web application library which adds high level abstractions to Compojure and Ring. The sandbar.auth namespace provides code to make role-based security simple.

Security can be divided into two essential parts: authentication and authorization. In this post we will only explore authorization and are therefore getting only part of the security story. In part two we will add encryption, channel security and form based authentication. In part three we will authorize based on URI patterns. Finally, in the fourth installment, we will use Sandbar to create a user administration tool.

The complete code for this example is located in the sandbar-examples repository under security. If you would like to follow along then clone that repository, get the dependencies, start a REPL and open the sandbar.examples.part-one.start namespace in your favorite editor.

$ git clone git://github.com/brentonashworth/sandbar-examples.git
$ cd sandbar-examples/security
$ open src/sandbar/examples/part_one/start.clj
$ lein deps
$ lein repl


user=> (use ‘sandbar.examples.part-one.start)
user=> (run)

We will start with a small Compojure application and, throughout this series, add features to it. The complete source for our starting point is shown below.


(ns sandbar.examples.part-one.start
 (:use (ring.adapter jetty)
       (compojure core)
       (hiccup core page-helpers)))

(defn query [type]
 (str (name type) " data"))

(defn layout [content]
 (html
  [:html
    [:body
    [:h2 "Sandbar Authorization Example"]
    content]]))

(defn data-view [title data & links]
 [:div
  [:h3 title]
  [:p data]
  (if (seq links) links [:div (link-to "home" "Home")])])

(defn home-view []
 (data-view "Home"
            (query :public)
            [:div (link-to "member" "Member Data")]
            [:div (link-to "admin" "Admin Data")]))

(defn member-view []
 (data-view "Member Page"
            (query :members-only)))

(defn admin-view []
 (data-view "Admin Page"
            (query :top-secret)))

(defroutes my-routes
 (GET "/home*" [] (layout (home-view)))
 (GET "/member*" [] (layout (member-view)))
 (GET "/admin*" [] (layout (admin-view)))
 (ANY "*" [] (layout (home-view))))

(def app my-routes)

(defn run []
 (run-jetty (var app) {:join? false :port 8080}))

(The app var is not required here but having it allows us to load this code once and then make all of the changes below without having to restart our REPL.)

This is a complete application which will show three views: the home page will show public data, the member page will show members-only data and the admin page will show top-secret data. The query function retrieves the data that is displayed.

Add sandbar.auth, middleware and create an authentication function


We would like to have two roles: administrators and members. Only administrators may see top-secret data and only members may see members-only data. We would also like our application to have different behavior based on a user's role.

Before we may add any of Sandbar's functionality, we need to first add Sandbar to our namespace declaration.


(:use (ring.adapter jetty)
       (compojure core)
       (hiccup core page-helpers)
       (sandbar stateful-session auth))

Here we have added the sandbar.stateful-session and sandbar.auth namespaces.

Next, we add some middleware.


(def app
    (-> my-routes
        (with-security authenticate)
        wrap-stateful-session))

Our routes have been wrapped with the with-security and wrap-stateful-session middleware. with-security does all of the hard work. Here it has one argument, an authentication function, which we have not written. The wrap-stateful-session middleware is used by with-security to store user information.

authenticate is where you would do whatever you need to do to figure out who the current user is based on the information in the request. This function is very simple, it takes the request map as an argument and returns a single user map or nil. The user map has two keys: :name, which is a string, and :roles, which is a set of role keywords. For this example, we will use the function below.


(defn authenticate [request]
 (let [uri (:uri request)]
   (cond (= uri "/member") {:name "joe" :roles #{:member}}
         (= uri "/admin") {:name "sue" :roles #{:admin}})))

This function will not secure our application but it does demonstrate how simple it is to add your own authentication scheme. For our example, we simply authenticate the user based on the first URI that is accessed. This function will only be called if the current user is not authenticated.

After making these changes we should still have a working application. Reload the namespace in the REPL and try it out.


user=> (use :reload-all ‘sandbar.examples.part-one.start)

There is a lot of new code running, but the functionality has not changed. At least we know we haven't broken anything.

Protecting data in the model


All of our data is still out in the open. To protect it, we could use the ensure-any-role macro. It takes two arguments: a set of roles and a form that is being protected. The protected form will only be executed if the user is authenticated and has one of the roles in the set.


(defn query [type]
 (ensure-any-role #{:member :admin}
                  (str (name type) " data")))

This isn't really what we are looking for. A simple visit to the home page will result in a java.lang.StackOverflowError. Understanding why will help us understand what is going on behind the scenes. ensure-any-role will throw an exception when the current user is not authenticated. The with-security middleware will catch the exception and then call authenticate to authenticate this user. Our authentication function will not authenticate the user becuase it only authenticates visitors to the admin and member pages. Finally, with-security will redirect the user to the home page and the cycle continues.

This is probably a bug which needs fixing, but it does indicate that we have a problem. There is no way for an unauthenticated user to view the public data on the home page. We could start using conditionals but that will get ugly. Instead, there is a better macro to use in this situation named ensure-any-role-if.


(defn query [type]
 (ensure-any-role-if (= type :top-secret) #{:admin}
                     (= type :members-only) #{:member}
                     (str (name type) " data")))

This macro takes an odd number of arguments. The final argument is the form that you are protecting. The other arguments are pairs of predicates and sets of roles. If the predicate is true then the user must be a member of one of the roles in the corresponding set. If none of the predicates are true then the protected code will be run without authentication.

We can safely try it now. If you first click on the "Member Page" link you will see the members-only data. If you then try to click on the "Admin Page" link, nothing will happen. Our data has been protected.

Logout and permission denied


When ensure-any-role or ensure-any-role-if notice that the current user is authenticated but does not have the correct role, it will redirect the user to "/permission-denied". It would be nice if we could see a better permission denied page. Add the following route and implementation of the permission-denied-view function.


(GET "/permission-denied*" [] (layout (permission-denied-view)))


(defn permission-denied-view []
 [:div
  [:h3 "Permission Denied"]
  [:div (link-to "home" "Home")]])

We may also want to logout so that we can easily try out the different roles.


(GET "/logout*" [] (logout! {}))

The logout route will use a function named logout! from sandbar.auth to clear the current user from the session. The parameter to logout! is a map which may contain the key :logout-page. If this key is not found then the user will be redirected to “/” after logout.

The user will need some kind of link to allow them to logout easily. Add the following div to the end of the body section of the layout.


[:div (if-let [username (current-username)]
        [:div
          (str "You are logged in as " username ". ")
          (link-to "logout" "Logout")])]

The current-username function is another function from sandbar-auth. It will get the current user's username from the session or return nil if one does not exist. After reloading in the REPL, we now see the username at the bottom of the page, we may logout and we have a proper permissions denied page.

Checking the role


As a final flourish for this example we will add the following div to our layout underneath the content and above the logout code that we added above.


[:div (cond (any-role-granted? :admin)
            "Hello administrator!"
            (any-role-granted? :member)
            "Hello member!"
            :else "Click on one of the links above to log in.")]

This shows how to use the any-role-granted? predicate. Here we show a different message based on the role of the current user. any-role-granted? may be passed a single role or a set of roles. It will return true if the user is a member of one of those roles.

The complete source code for the new authorized version is located in the namespace sandbar.examples.part-one.complete.

Conclusion


We haven’t actually secured anything in this example. But hopefully you can see how that would be done by creating a different implementation of authenticate. In the next installment, we will look at one way to do this and add form based authentication to this example project as well as encryption and channel security.

For more information on Sandbar, see the wiki and check out the source code. The source for sandbar.auth is only 245 lines. Sandbar is still quite new and needs a lot of work. I hope this series will draw more attention to it and help to make it better.

1 comment:

  1. Nice tutorial. It all worked. I look forward to doing the rest of them.

    ReplyDelete