Drum roll, please… After more than a year, I finally have some time to pick up this blog series on creating RESTful data APIs with Clojure. It is amazing to me how much the Clojure ecosystem has changed (for the better) even in the past year. One of these changes has been the release of the the Buddy security library Okay, it has been around for over a year, but in the past few months or so, Buddy has become the de-facto library for Clojure web app security.
Originally, I had planned on using Friend - which I still think is a great library - but Buddy is far more flexible and jives well with the Clojure community’s focus of building amazing things from small, composable parts. Buddy consists of modules for crypto, auth/auth, password hashing, and message signing, which may be used together or a la carte. For our shopping list API, we will use the auth and password hashing functionality of Buddy. We will only be scratching the surface of both web security in general and the Buddy library specifically, but I have listed some Resources at the end of this article that are definitely worth a read/watch.
Security Architecture
There are a number of options for securing a web service or data API. One of the main considerations is whether the API will be consumed by other back-end services or by front-end applications. In our case, the API will be consumed by a JavaScript application directly, so we will provide a mechanism that will be simple for that sort of client to consume.
We will introduce two different user types - :user
and :admin
- who will each
have different permissions in the system. Admins should be able to do everything
that regular users can as well as a few more privileged actions. Users will authenticate
with the system by providing an auth token in an Authorization
HTTP header,
which the server will verify to determine whether the auth token is valid
and which user it is associated with.
Note that for an application with higher security requirements, this approach would probably not be sufficient because if a hacker got the auth token, they could have access to the system without even knowing the identity of the user that they were impersonating. However, as long as the initial token request as well as every single request containing the token happens over HTTPS, we don’t need to be too worried.
If a logged-out client requests a resource that requires authentication, or if a
logged-in client an requests a resource that they are not authorized for, an
HTTP 401 Unauthorized
response should be returned. Eventually, we would probably
want to return the 401 only for unauthenticated requests and use an
HTTP 403 Forbidden
instead for requests that to not have adequate authorization,
but to make things simple for this tutorial, we’ll stick with returning the 401
in both cases.
Code, glorious code
With those high-level requirements outlined, let’s start turning them into tests. The previous tutorials were pretty code-heavy, but by now I assume that you know how the project is organized, so I’ll only include the most pertinent portions of code. The full source code is available on the companion repo.
First, we’ll implement password hashing for users. We want to be able to validate a user’s password without storing their password in the database in plain text. Buddy’s hashers module is perfect for this.
Passwords for users
Before we can write the code to get these tests passing, we need to create a
database migration adding a password_digest
column to the users
table. We also
need to add the buddy-hashers dependency to our project.clj
. Once those steps
are complete, we’re ready to update the restful-clojure.models.users
namespace.
Most of this is pretty standard stuff for a web app, but notice that we are taking care that the password is hashed and stored in the database but that even the hash is never exposed to the client. When you run the tests now, you’ll also notice that they are much slower. This is due to the fact that the bcrypt algorithm that Buddy uses by default to hash passwords is slow. Slowness is a good thing when it comes to password hashing because the slower a hash algorithm is, the less effective it renders brute-force attacks.
Auth tokens
The next order of business is allowing users to supply their user id and password and get an auth token that is valid for some specific amount of time. This time I’ll skip the tests because they are not very interesting.
We’ll need to create a new migration that will create an auth_tokens
table:
While we’re at it, let’s go ahead and create another migration adding a level
column to the users table to identify a user a being a “user” or “admin”.
We will bundle both authorization and authentication together into one “auth” namespace. I’ll show you the full code below, and then we’ll walk through what it does. This is probably the longest code sample in the tutorial, but it contains most of the internals of auth, so try to follow along.
First, we create a utility function for generating session identifiers, which
are cryptographically strong random bits that are base-64 encoded. While it would
be easier to use something like a UUID here, that would create a guessable
session identifier, making it much easier for a hacker to exploit the system. I
am using James Reeve’s crypto-random
library here, but any strong random generator is okay here (even pulling bytes
off of /dev/urandom
).
Next, the make-token!
function simple creates a new session in the database
associated with the given user id.
We then create a Buddy token-based authentication backend, that will hook into
our Ring middleware and look for a “Authorization” HTTP header, extracting the
token from that. If a valid token is found, Buddy will associate the returned
user in the Ring request map, which we can use to either require that the user is
logged-in or that they have a specific user level. The Buddy middleware will call
the authenticate-token
function with the Ring request map and the token found
in the “Authorization” header and will expect a user if the token was valid and
nil
otherwise.
Finally, we create a simple permission structure such that users can only manage lists (create, add/remove products, etc.), and admins can manage products and users in addition to everything that users can do. In order to create the user hierarchy such that admins will inherit all user privileges, we will create an ad-hoc hierarchy in the users namespace. We also need to make a few changes additional changes to accommodate the addition of user levels. Most of the relevant code from the users namespace is below.
The two changes here are that we create a hierarchy of ::user
s and ::admin
s
and that we convert between string representations for storage and keyword
representations for programmatic operation. Another minor detail is that we
ensure users with no level specified are always stored with the “user” type.
Finally, we make the necessary changes in our handlers to restrict endpoints to logged-in users/users with the appropriate privileges. This is a good time to refactor the handler namespace to extract the business logic for each route to its own function. As the app grows, these will probably move into other namespaces, so for now having the business logic isolated from the routing logic will come in handy. With the routes cleaned up, it is now easier to see how the various authorization rules play out with our application routes.
I admit that some of the nesting of routes within contexts can get a little ugly. Many developers prefer to write “flat” routes with a little more duplication, which is admittedly easier to read, but it can become more difficult to maintain with code duplicated between a number of route definitions.
Note, however, how nicely we can express our auth requirements, as in the following example extracted from the routes above:
This very declaratively expresses that, “For finding, updating, and deleting a specific list, we need the user to be authenticated and either be able to manage lists or be the owner of the specific list”. This, I think, is where Clojure really shines - declarative APIs expressed as data. Did you notice that we lay out our auth rules as some nested maps and vectors? I’ll take that over an imperative auth system any day!
In Conclusion
I have only briefly glossed over a couple of the many security concerns that web apps face. Please know that this is only the tip of the iceberg and that there is much more to building a “bulletproof” web service that what was covered here. That said, the authorization and authentication practices in this tutorial should pretty much cover what you’ll need to build into a typical web service or application of this scale.
If you do not have the code for this tutorial, I’d recommend checking it out from its GitHub repo and playing around with it. If you have been walking through these tutorials from part 1, then you should now have a complete, albeit simple, API server that you can deploy and build a front-end app on!
Next up, we’ll build a simple front-end app to consume our web service, then finally, we will deploy it to a DigitalOcean droplet. Stay tuned!
Resources:
- Securing Clojure Microservices using buddy - Part 1: Creating Auth Tokens
- (Video) Aaron Bedra - clojure.web/with-security
- OWASP Top 10
Go To
- Part 1: Setup
- Part 2: Getting a web server up and running
- Part 3: Building out the web service
- Part 4: Securing the service