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.