sandbar.auth may be used to add form-based authentication and channel security.Form-based authentication and channel security
If you would like to follow along:
$ git clone git://github.com/brentonashworth/sandbar-examples.git
$ cd sandbar-examples/security
$ open src/sandbar/examples/part_two/start.clj
$ lein deps
This code is a bit different from what we ended with last time. A stylesheet has been added and the layout has been improved. The complete source for our starting point is shown below. Make sure you understand this code before moving on.
(ns sandbar.examples.part-two.start
(:use (ring.adapter jetty)
(ring.middleware file)
(compojure core)
(hiccup core page-helpers)
(sandbar core stateful-session auth)))
(defn query [type]
(ensure-any-role-if (= type :top-secret) #{:admin}
(= type :members-only) #{:member}
(str (name type) " data")))
(defn layout [content]
(html
(doctype :html4)
[:html
[:head
(stylesheet "sandbar.css")
(icon "icon.png")]
[:body
[:h2 "Sandbar Security Example"]
content
[:br]
[:div (if-let [username (current-username)]
[:div
(str "You are logged in as " username ". ")
(link-to "logout" "Logout")])]]]))
(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")]
[:br]
[: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.")]))
(defn member-view []
(data-view "Member Page"
(query :members-only)))
(defn admin-view []
(data-view "Admin Page"
(query :top-secret)))
(defn permission-denied-view []
[:div
[:h3 "Permission Denied"]
[:div (link-to "home" "Home")]])
(defroutes my-routes
(GET "/home*" [] (layout (home-view)))
(GET "/member*" [] (layout (member-view)))
(GET "/admin*" [] (layout (admin-view)))
(GET "/logout*" [] (logout! {}))
(GET "/permission-denied*" [] (layout (permission-denied-view)))
(ANY "*" [] (layout (home-view))))
(defn authenticate [request]
(let [uri (:uri request)]
(cond (= uri "/member") {:name "joe" :roles #{:member}}
(= uri "/admin") {:name "sue" :roles #{:admin}})))
(def app (-> my-routes
(with-security authenticate)
wrap-stateful-session
(wrap-file "public")))
(defn run []
(run-jetty (var app) {:join? false :port 8080}))
Encryption
Our application must ensure that passwords are not sent across the network in plain text. In a production environment, one would enable SSL support on the web server and purchase a legitimate SSL certificate from a valid authority for use with the site's domain. For development, it is good enough to create a self-signed certificate and turn on Jetty's SSL support.
Use Java's
keytool to create a self-signed certificate.
$ keytool -genkey -alias sandbar -keyalg RSA -keystore my.keystore -keypass foobar
Enter keystore password:
Re-enter new password:
What is your first and last name?
[Unknown]: localhost
What is the name of your organizational unit?
[Unknown]: dev
What is the name of your organization?
[Unknown]: clojure
What is the name of your City or Locality?
[Unknown]: New York
What is the name of your State or Province?
[Unknown]: New York
What is the two-letter country code for this unit?
[Unknown]: US
Is CN=localhost, OU=dev, O=clojure, L=New York, ST=New York, C=US correct?
[no]: y
For this example, the password "foobar" was entered.
keytool has created a keystore in the file named my.keystore. Make sure this file is located in the root directory of the security module (at the same level as the public directory).To make use of this keystore, update the
run function so that it matches the version shown below.
(defn run []
(run-jetty (var app) {:join? false :ssl? true :port 8080 :ssl-port 8443
:keystore "my.keystore"
:key-password "foobar"}))
Setting
:join? to false will cause the call to run-jetty to return so that the REPL may still be used. The :ssl-port defaults to 443; here we set it here to 8443. Everything else is straight forward. If you are following along, now would be a great time to start a REPL and test that everything is working as expected.
$ lein repl
user=> (use 'sandbar.examples.part-two.start)
user=> (run)
Navigating to
https://localhost:8443/ and http://localhost:8080/ confirms that the application may be used over SSL or standard http and that everything works the same as it did before.Note: You will get a warning message because the certificate that we are using is not legitimate. This is fine for development; do what you need to do to add an exception for this certificate.
Adding form-based authentication
In the last post, the
with-security middleware was added and configured to use our authenticate function. In this section, the authenticate function will be replaced with an authentication function from sandbar.form-authentication and a pre-built login form will be added. Start by adding the required namespaces
sandbar.form-authentication and sandbar.validation.Delete the
authenticate function and replace authenticate with form-authentication in our with-security middleware. form-authentication will redirect a user to a login form when that user is not authenticated.To implement the login form, add
form-authentication-routes to the list of routes.
(form-authentication-routes (fn [_ c] (layout c))
(form-authentication-adapter))
The parameters to
form-authentication-routes are: a layout function and something that satisfies the protocol FormAuthAdapter. The layout function must take two parameters: the request and the content to layout. The FormAuthAdapter protocol specifies two functions which allow us to adapt this component to our system. The functions are load-user and validate-password. load-user takes a username and password and returns a user map. A user map must at least have the keys :username and :roles. validate-password returns a function that can validate the user map created by load-user.
(defrecord DemoAdapter []
FormAuthAdapter
(load-user
[this username password]
(let [login {:username username :password password}]
(cond (= username "member")
(merge login {:roles #{:member}})
(= username "admin")
(merge login {:roles #{:admin}})
:else login)))
(validate-password
[this]
(fn [m]
(if (= (:password m) (:username m))
m
(add-validation-error m "Username and password do not match!")))))
(defn form-authentication-adapter []
(DemoAdapter.))
This implementation of
load-user will simply look at the username to determine if the user is a member or admin. The validate-password implementation will ensure that the username and password are the same. add-validation-error is a function from sandbar.validation which is being used here to display an error message. (For more information about validators, see the post Clojure Macros Make Me Happy. I will not go into any more detail here.)
After making these changes, saving them and reloading the namespace,
user=> (use :reload-all 'sandbar.examples.part-two.start)
we can return to our application where we should now have an operational login form. Try submitting the form while it is empty. Try entering a username and password that do not match. The form does the right thing in each situation.
Form Customization
At this point you may want to customize the field names on the form as well as the error messages that are displayed. While we are at it, let's make a custom logout landing page. Create a map with keys that correspond to the fields and errors that we would like to update as well as the key
:logout-page that indicates where to go after we logout.
(def properties
{:username "Username"
:password "Password"
:username-validation-error "Enter either admin or member"
:password-validation-error "Enter a password!"
:logout-page "/after-logout"})
Update the
form-authentication-adapter constructor to merge these properties into our DemoAdapter. Because Clojure's records implement the persistent map interface, the form-authentication module uses the FormAuthAdapter as a map to look up field names and error messages for the login form.
(defn basic-auth-adapter []
(merge (DemoAdapter.) properties))
After making this change, we will see our own field names and error messages displayed on the login form.
Next, create the logout landing page by replacing the empty map that was passed to
logout! with properties and then creating a route
(GET "/after-logout" [] (layout (after-logout-view)))
and view.
(defn after-logout-view []
[:div
[:h3 "Logout"]
[:p "You are no longer logged in!"]
[:div (link-to "home" "Home")]])
In all the changes that have been made so far, a pattern is emerging; add components in the form of middleware or parametrized routes then adapt it to our project. This same pattern will be used in the next section to add channel security.
Channel security
One thing that you may have noticed is that even though SSL is enabled, there is no way to control when it is used and when not. The goal in this application is to secure passwords sent from the client to the server and, because of the additional delay, to use SSL on as few pages as possible. This may be done by adding the
with-secure-channel middleware which takes four parameters: the routes to be wrapped, a security configuration, the port and the SSL port. After adding this middleware, the app var definition now looks like this:
(def app (-> my-routes
(with-security basic-auth)
wrap-stateful-session
(wrap-file "public")
(with-secure-channel security-config 8080 8443)))
The security configuration
security-config is a vector of pairs. Each pair is a regular expression literal followed by a configuration. We use a vector here instead of a map because each entry is checked against the current URI in order with the first match being selected.
(def security-config
[#"/login.*" :ssl
#".*.css|.*.png" :any-channel
#".*" :nossl])
Three keywords are used to represent the three kinds of channel security:
:ssl, :nossl and :any-channel. The above configuration causes the login screen to always be accessed through SSL, images and stylesheets to go over any channel and everything else to go through standard http.A future post will show how this same vector may be used to authorized access based on URI patterns.
If you did not follow along, you may want to take a look at the final version of the source for this exmaple.
Conclusion
Progress has been made, but this app is still not secure. Passwords should not be hard coded into an application like this. The application may be improved by having a way to easily assign different passwords to each user and store them securely. There will be at least two more posts in this series; one them will cover some of the new features of Sandbar which can help make this easy for the most common cases. The other will demonstrate how to authorize access to resources based on URI patterns.
