I am writing a Compojure application that will host my journal on EricLavigne.net. In a previous article, I showed how to use PostgreSQL with Compojure. Now that journal articles are stored in the database, it should be possible to add or update journal articles through the website, but I want to make sure that only I can do that. The next step, then, is to deal with security issues, such as authentication, authorization, and encryption. While we’re on the topic of security, I’ll also discuss automatic updates.
Automatic updates
Keeping your computer up-to-date is an important part of security, and is easily accomplished with automatic updates. The idea is described more thoroughly in this article.
Cron is a program that allows you to schedule commands (such as a system update) to be run routinely. The first step is to run the following command.
crontab -e
After running that command, you will be in file editing mode. Type the following line as the last line of that file, then save the file.
0 1 * * * (aptitude -y update && aptitude -y safe-upgrade) 2>&1 >> /var/log/auto_update.log
The line that you added meant that at 1:00 AM (minute 0 of hour 1) of every day of the month (*) and every month (*) and every day of the week (*) you want to run an update command (aptitude -y update …) and record the results of that command in /var/log/auto_update.log.
Password authentication
In this section, I create a login form with username and password fields. In a typical web application, the username and password for each visitor would be stored in the database. A web application for hosting my journal really only needs one authenticated user, so one hard-coded password is enough. For the purpose of demonstration I will follow a path between these two extremes, allowing users to choose any username but setting one hard-coded password.
There is already a working application described in a previous article, so I will focus here on those things that I changed to add authentication: a login form, controllers for logging in and logging out, URL routing for these new components, and a standard page layout that provides access to the new components.
The login view is a form with two text fields, one for the username and one for the password, and a “Log in” button which sends this information to the login controller. The password text field needs type “password” rather than “text” so that the password is not displayed when typed.

(defn login-view []
(html
[:form {:method "post"}
"User name: "
[:input {:name "name", :type "text"}]
[:br]
"Password: "
[:input {:name "password", :type "password"}]
[:br]
[:input {:type "submit" :value "Log in"}]]))
The login controller receives the username and password from the login form, checks whether these are valid credentials and, if they are valid, authenticates the user by adding the username to the session. Data entered into the login form is stored in params. The session is a reference to a map and can be used to store arbitrary information about one visitor to the website.
Typically, validating a user’s credentials means checking whether some user record in the database has that combination of username and password. Instead, this login controller only checks that the username contains typical characters (letters, numbers, spaces, underscores, and hyphens) and that the password is “secret”.
(defn login-controller [session params]
(dosync
(if
(and
(= "secret" (params :password))
; Username can include letters, numbers,
; spaces, underscores, and hyphens.
(.matches (params :name) "[\\w\\s\\-]+"))
(do
(alter session assoc :name (params :name))
(redirect-to "/articles/"))
(redirect-to "/login/"))))
The logout controller is very simple – just remove the user’s name from the session so that they are no longer logged in.
(defn logout-controller [session]
(dosync
(alter session assoc :name nil)
(redirect-to "/articles/")))
The URL routing needs three new lines for the login view, login controller, and logout controller. Note that the difference between login view and login controller comes down to the method: GET or POST.
(defservlet journal-servlet
"Eric Lavigne's Journal"
(ANY "/articles/" (view-article-list session))
(ANY "/articles/:title"
(view-article session (route :title)))
(GET "/login/" (login-view))
(POST "/login/" (login-controller session params))
(ANY "/logout/" (logout-controller session))
(ANY "/*" (redirect-to "/articles/")))
Each page should tell a user whether they are logged in. If they aren’t logged in, they should have a link to the login page. If they are logged in, they should have a link to logout. Now that there are components that belong on every page, it is time to create a standard page layout to avoid repetition.
(defn page [session title body]
(html
[:html
[:head [:title title]]
[:body
[:h1 title]
body
[:p
(if (@session :name)
(link-to "/logout/"
(str "Log out " (@session :name)))
(link-to "/login/" "Log in"))]]]))
The same page layout is used for the article list and for individual articles, and will probably be used for most new pages on the site as well. Besides reducing the amount of code required to define a view, this will give the site a more uniform look. As an example, this is how the article list looks after applying the new page layout.



(defn view-article-list [session]
(page session "Articles"
[:dl (mapcat
(fn [article]
(list
[:dt (render-article-link article)]
[:dd (article :description)]))
(fetch-articles))]))
Now that password authentication is implemented, site.clj includes the following code. The next step will be authorization for the admin page.
(ns ericlavigne
(:use compojure.http)
(:use compojure.html)
(:require [compojure.jetty :as jetty])
(:require [clojure.contrib.sql :as sql]))
(defn article-title-to-url-name [title]
(.replaceAll (.toLowerCase title) "[^a-z0-9]+" "-"))
(defn article-url [article]
(str
"/articles/"
(article-title-to-url-name
(article :title))))
; Database
(def db {:classname "org.postgresql.Driver"
:subprotocol "postgresql"
:subname "production"
:user "postgres"})
(defn sql-query [query]
(sql/with-connection db
(sql/with-results res
query (into [] res))))
(defn fetch-articles []
(sql-query "select * from article"))
(defn fetch-article [title]
(first
(filter
(fn [art]
(=
(article-title-to-url-name title)
(article-title-to-url-name (art :title))))
(fetch-articles))))
; Renderers
(defn render-article [article]
[:div
[:p [:em (article :description)]]
(article :body)])
(defn render-article-link [article]
(link-to
(article-url article)
(article :title)))
; Layout
(defn page [session title body]
(html
[:html
[:head [:title title]]
[:body
[:h1 title]
body
[:p
(if (@session :name)
(link-to "/logout/"
(str "Log out " (@session :name)))
(link-to "/login/" "Log in"))]]]))
; Views
(defn view-article [session title]
(try
(let [article (fetch-article title)]
(page session (article :title)
(render-article article)))
(catch Exception ex
(redirect-to "/articles/"))))
(defn view-article-list [session]
(page session "Articles"
[:dl (mapcat
(fn [article]
(list
[:dt (render-article-link article)]
[:dd (article :description)]))
(fetch-articles))]))
(defn login-view []
(html
[:form {:method "post"}
"User name: "
[:input {:name "name", :type "text"}]
[:br]
"Password: "
[:input {:name "password", :type "password"}]
[:br]
[:input {:type "submit" :value "Log in"}]]))
; Controllers
(defn login-controller [session params]
(dosync
(if
(and
(= "secret" (params :password))
; Username can include letters, numbers,
; spaces, underscores, and hyphens.
(.matches (params :name) "[\\w\\s\\-]+"))
(do
(alter session assoc :name (params :name))
(redirect-to "/articles/"))
(redirect-to "/login/"))))
(defn logout-controller [session]
(dosync
(alter session assoc :name nil)
(redirect-to "/articles/")))
; Routing
(defservlet journal-servlet
"Eric Lavigne's Journal"
(ANY "/articles/" (view-article-list session))
(ANY "/articles/:title"
(view-article session (route :title)))
(GET "/login/" (login-view))
(POST "/login/" (login-controller session params))
(ANY "/logout/" (logout-controller session))
(ANY "/*" (redirect-to "/articles/")))
; Starting the service
(jetty/defserver journal-server
{:port 80}
"/*" journal-servlet)
(jetty/start journal-server)
Admin page authorization
Now it’s time to create the admin page and ensure that only an authorized user (admin) can access that page.
The admin user should have a link to get to the admin page. We can put that link in the standard page layout so that it’s always available, but only to the admin user.

Someone who is not logged in gets a “Log in” link. Someone who is logged in as “admin” gets links to both “Log out admin” the new admin page. Everyone else just gets a link to “Log out <name>”.
(defn page [session title body]
(html
[:head [:title title]]
[:body
[:h1 title]
body
(dosync
(cond
(not (@session :name))
[:p (link-to "/login/" "Log in")]
(= (@session :name) "admin")
[:div
[:p (link-to "/admin/" "Admin page")]
[:p (link-to "/logout/" "Log out admin")]]
:else
[:p (link-to "/logout/"
(str "Log out " (@session :name)))]))]))
At this point, only the admin user has a link to the admin page, but that doesn’t stop other users from typing the URL into their address bars. We need to protect the admin page from non-admin users. This is most easily done by checking, in the admin view, whether the user is logged in as admin. The problem with that is that later there will be more than one page that is restricted to admin, so that code doesn’t belong in any individual view.
Instead, we can use a controller that checks whether the user is logged in as admin. If the user is logged in as admin, the ensure-admin controller returns the :next keyword to let control pass to another controller or view (such as the admin view). Otherwise, the user is redirected back to the main page.
(defn ensure-admin-controller [session]
(dosync
(if (and (@session :name) (= (@session :name) "admin"))
:next
(redirect-to "/login/"))))
The new URL routing shows how the ensure-admin controller is used. The ensure-admin controller appears near the end of the URL routing and matches any URL. This means that any request that doesn’t match before that point will be sent to the ensure-admin controller. Non-admin users are redirected at that point, so any URL routing that comes after the ensure-admin controller, such as the admin view, will only be accessible to the admin user.
(defservlet journal-servlet
"URL routing for Eric Lavigne's Journal"
(ANY "/articles/" (view-article-list session))
(ANY "/articles/:title"
(view-article session (route :title)))
(GET "/login/" (login-view session))
(POST "/login/" (login-controller session params))
(ANY "/logout/" (logout-controller session))
(ANY "/*" (ensure-admin-controller session))
(ANY "/admin/" (admin-view session))
(ANY "/*" (go-home)))
Finally, we need to define the admin view. It can be very simple for now, as the focus is on whether we can protect this view from unauthorized users.

(defn admin-view [session]
(page session "Admin"
[:p "Only admin can see this page."]))
Now that password authentication is implemented, site.clj includes the following code. [skip past code]
(ns ericlavigne
(:use compojure.http)
(:use compojure.html)
(:require [compojure.jetty :as jetty])
(:require [clojure.contrib.sql :as sql]))
(defn article-title-to-url-name [title]
(.replaceAll (.toLowerCase title) "[^a-z0-9]+" "-"))
(defn article-url [article]
(str
"/articles/"
(article-title-to-url-name
(article :title))))
(defn go-home []
(redirect-to "/articles/"))
; Database
(def db {:classname "org.postgresql.Driver"
:subprotocol "postgresql"
:subname "production"
:user "postgres"})
(defn sql-query [query]
(sql/with-connection db
(sql/with-results res
query (into [] res))))
(defn fetch-articles []
(sql-query "select * from article"))
(defn fetch-article [title]
(first
(filter
(fn [art]
(=
(article-title-to-url-name title)
(article-title-to-url-name (art :title))))
(fetch-articles))))
; Renderers
(defn render-article [article]
[:div
[:p [:em (article :description)]]
(article :body)])
(defn render-article-link [article]
(link-to
(article-url article)
(article :title)))
; Layout
(defn page [session title body]
(html
[:head [:title title]]
[:body
[:h1 title]
body
(dosync
(cond
(not (@session :name))
[:p (link-to "/login/" "Log in")]
(= (@session :name) "admin")
[:div
[:p (link-to "/admin/" "Admin page")]
[:p (link-to "/logout/" "Log out admin")]]
:else
[:p (link-to "/logout/"
(str "Log out " (@session :name)))]))]))
; Views
(defn view-article [session title]
(try
(let [article (fetch-article title)]
(page session (article :title)
(render-article article)))
(catch Exception ex
(go-home))))
(defn view-article-list [session]
(page session "Articles"
[:dl (mapcat
(fn [article]
(list
[:dt (render-article-link article)]
[:dd (article :description)]))
(fetch-articles))]))
(defn login-view [session]
(page session "Log in"
[:form {:method "post"}
"User name: "
[:input {:name "name", :type "text"}]
[:br]
"Password: "
[:input {:name "password", :type "password"}]
[:br]
[:input {:type "submit" :value "Log in"}]]))
(defn admin-view [session]
(page session "Admin"
[:p "Only admin can see this page."]))
; Controllers
(defn login-controller [session params]
(dosync
(if
(and
(= "secret" (params :password))
; Username can include letters, numbers,
; spaces, underscores, and hyphens.
(.matches (params :name) "[\\w\\s\\-]+"))
(do
(alter session assoc :name (params :name))
(go-home))
(redirect-to "/login/"))))
(defn logout-controller [session]
(dosync
(alter session assoc :name nil)
(go-home)))
(defn ensure-admin-controller [session]
(dosync
(if (and (@session :name) (= (@session :name) "admin"))
:next
(go-home))))
; Routing
(defservlet journal-servlet
"URL routing for Eric Lavigne's Journal"
(ANY "/articles/" (view-article-list session))
(ANY "/articles/:title"
(view-article session (route :title)))
(GET "/login/" (login-view session))
(POST "/login/" (login-controller session params))
(ANY "/logout/" (logout-controller session))
(ANY "/*" (ensure-admin-controller session))
(ANY "/admin/" (admin-view session))
(ANY "/*" (go-home)))
; Starting the service
(jetty/defserver journal-server
{:port 8080}
"/*" journal-servlet)
(jetty/start journal-server)
Next steps
I had originally intended to cover encryption with HTTPS. However, this turned out to be a rather difficult topic. Here are some of the resources I found on using HTTPS with Jetty – I may return to this issue for a later article.
Now that the admin page is password protected, it’s time to use that page for site administration, such as adding and editing articles. Adding and editing articles will be the topic of another article.
9 Comments
January 4, 2009 at 9:15 pm
Not bad. Ideally, of course, you’d store the hash of the password instead of the password itself, but looks good so far.
January 4, 2009 at 9:29 pm
Hi, Jordan. Good to hear from you.
I forgot about hashing the password. To my inexperienced eye, lack of encryption seems like the bigger security hole here, and I have zero experience with setting it up. Once this application is working to the point of being usable, I need to come back and fix both issues.
January 4, 2009 at 10:46 pm
Hi Eric,
Thanks for the posts! I think they’re a great entrypoint into Compojure.
One note: namespaces in Clojure have a code part & a filesystem part, where components in the namespace must map to components of the classpath-relative filesystem path: my.cool.ns must be in /my/cool/ns.clj. To work properly, the file containing your namespace declaration (ns ericlavigne) should therefore be called ericlavigne.clj. The current setup — site.clj — works so long as you’re calling it as a script, but if you want to refer to it from other code (i.e. namespaces), you’ll have to make sure the ns-filesystem mapping’s in place. See clojure.org/namespaces.
You may know all this already, but I didn’t want others to get tripped up.
Best,
Perry
January 5, 2009 at 4:40 am
Hi Perry.
I was aware that the issue existed but didn’t yet know the details. This setup worked well for the first two articles but, at 157 lines of code, I will soon need to break this program into several files.
January 5, 2009 at 3:51 pm
If you use a session store that’s client-side (such as simple cookies) then this is pretty insecure since the client can change its cookies arbitrarily. I’m not sure what Jetty uses by default.
One thing that I feel like I really don’t grasp yet is an understanding of when and how to split things out into namespaces and how to refer to them when they are. (When use is preferred over require; when you should just refer to a value with the namespace before it, etc) Might be a good thing to cover in a future installment since most compojure apps aren’t kept in a single file.
January 5, 2009 at 6:27 pm
Jetty uses an in-memory session store by default. The cookies only store the keys, which are generated with java.security.SecureRandom, so it’s pretty robust.
With regards to password hashing, it’s more secure to use an algorithm designed for the job. MD5s and other similar algorithms are designed to be fast – not the sort of thing you want for obscuring passwords – and without salting the same password creates the same hash every time, making it vulnerable to rainbow tables. BCrypt is a password hashing scheme that’s used to hash the passwords in most (all?) modern Unix-like OSes, so it’s pretty well tested. There’s a Java implementation of it called jBCrypt.
January 5, 2009 at 8:45 pm
Following suggestions by James on the mailing list, I’ll be changing this article to use some of Compojure’s built-in support for forms and extract the username in the URL handling rather than in the views.
Phil, when to split code into namespaces is not specific to Clojure, but it is definitely important. I’ll split this application into several files, each with a different namespace, and make that the topic of the next article.
Regarding :use vs :require, :use lets you code with less typing, but :require is easier to understand when reading later and will protect your code from namespace clashes if the public API of one of your libraries expands. In other words, :require is always better long term, so I will set a good example by removing :use in the next article.
February 8, 2009 at 9:29 am
Hi Eric. Thanks for these articles. I’m looking forward to the next one.
Is the current state of this application available in a publicly available source-code repository somewhere? The tall-and-skinny blog format is a bit of a hassle for reference.
February 8, 2009 at 10:42 am
John, I plan to add this code to github, but it’s not there yet.
I also need to update the security article to match the latest version of Compojure. James broke some of the namespaces into several smaller namespaces. I already updated the other two articles, but haven’t gotten to this one yet.
I’ll be delivering a Clojure presentation to a Java user group this Wednesday, which is why I haven’t had time for articles. I’ll post my slides later this week.
http://www.codetown.us/events/gatorjug-1