developers

Secure a Clojure Web API with Auth0

Learn more about how we can secure our Clojure Web API using Auth0

Dec 8, 20219 min read

In my previous article, we built a simple Web API in Clojure with the Pedastal framework. We covered some core concepts essential to know for writing Clojure and serving data on the web.

It was a great start! But the very next thing we should consider when building a Web API is making sure that it's secured.

Requirements

Of course, you'll want the source code that we ended up with at the close of the previous article. You'll also need to sign up for a free Auth0 account. You won't need a credit card, and this will be the simplest way for us to add basic Authentication.

Interceptors explained

We have already used Interceptors while building our application. From our previous article:

Simply put, they let you layer functions to create a "pipeline" to handle web requests.

They can also be used for responses, as we've seen with our JSON serialization example above.

Building a base interceptor

In Pedestal, an interceptor is nothing more than a map with a

:name
along with
:enter
,
:leave
, and
:error
keys. The name is (you guessed it) the name, while the last three items represent optional keys that receive a context map as an argument. Each of these keys is executed when the request is coming in when the response is being sent, and if an error happens along the way. You'll need to include at least one of these functions for this to count as an interceptor!

For instance, let's say we want to create an interceptor that inserts a specific response header for every response we send. To accomplish this, all we need to do is create a map with a function on the

:leave
keyword and associate the header:

(def header-interceptor {
 :name "header-interceptor"
 :leave (fn [ctx] (assoc-in ctx [:response :headers "AppName"] "Pedestal-Auth0-App"))})

Pedestal also offers helpers to write a specific type of interceptor without having to specify the entire map. Here's that same function using these helpers:

(require 'io.pedestal.interceptors.helpers :as helpers)
 
(def header-interceptor (helpers/on-response (fn [response] (assoc-in response [:headers "AppName"] "Pedestal-Auth0-App"))))

Armed with a basic understanding of interceptors in Pedastal, we can now set up an Auth0 account and then write our interceptor to validate any tokens provided in a request.

Validating a Token

In an OAuth-compliant application, Identity providers must provide information to validate JWT tokens that they issue. Many frameworks and languages create black boxes where you provide some configuration parameters, and they do the rest. We're going to go into a bit more detail in this application. Here are some basic concepts to understand:

JSON Web Tokens

Most documentation sources and developers shorten JSON Web Tokens to just JWT. JWTs are essentially long strings that encode JSON, and are frequently used for Authentication and Authorization. Identity providers issue JWTs and include information relevant to the apps that use them, such as your name, email address, or permissions for that app. If you're unfamiliar with JWTs I highly suggest Auth0's documentation on the matter. When issued by an Identity provider, they're often signed so that you can know they're authentic, which leads us to...

JSON Web Key Set

JSON Web Key Set's acronym is JWKS. We'll use the acronym for the remainder of this article. This key set contains the public keys that say, "Yeah, I made that token." The Identity provider has used a separate and private cryptographic key to construct the token. This process that allows for a public verification key and a private construction key is called asymmetric encryption.

We'll need to retrieve the location of the JSON Web Key set for our Identity provider when we create a new Application in the Auth0 portal so we can verify JWTs. You can read a bit more about the concept in the Auth0 documentation.

Create a new Auth0 application

Before writing the code to validate a token issued by an identity provider effectively, let's quickly review a couple of steps necessary to set up Auth0.

Once we've logged in, select Applications -> APIs from the left menu and create a new API. For demo purposes, we can just put a random name and an identifier. In my case, I am going to be using

clojure-pedestal
for both.

Creating a new API in Auth0

When confirming the operation, Auth0 will also create a default Test application for the new API. Click on Applications -> Applications on the left menu to review the application's details. From the Settings tab, we will need to grab the domain. This is the base URL we'll need to determine a JWKS location.

The Application settings we'll need to configure in Clojure

In this example, since my domain is https://dev-q6rh8--8.us.auth0.com/, my JWKS URL will be https://dev-q6rh8--8.us.auth0.com/.well-known/jwks.json

Set up the authorization interceptor in Pedestal

Auth0 offers several Java libraries to work with JWT and JWK. Since Clojure has a good interoperability story with Java, we can use these libraries to write our interceptor.

If you've been writing Clojure for a little bit this should be simple. We will take it easy for this article and use a third-party Clojure library that will do most of the work. If you're curious/eager to see what using the Auth0 Java libraries would look like, you can take a look at the reference implementation that accomplishes the same thing at the end of this article.

The first thing we're going to do is to add a library providing the helpers we need to decode and verify the JWT in

deps.edn
:

{:paths ["src"]
:deps {io.pedestal/pedestal.service {:mvn/version "0.5.9"}
       io.pedestal/pedestal.route {:mvn/version "0.5.9"}
       io.pedestal/pedestal.immutant {:mvn/version "0.5.9"}
       org.slf4j/slf4j-simple {:mvn/version "1.7.32"}
-        environ/environ {:mvn/version "1.2.0"}}}
+        environ/environ {:mvn/version "1.2.0"}
+        no.nsd/clj-jwt {:git/url "https://gitlab.nsd.no/clojure/clj-jwt.git" :git/sha "bc23acb3c7cbf0d2def2d395c3e3d9c405be28d5"}}}

We can see that the

clj-jwt
has been imported from the repository directly instead of the Maven Central registry. This is a legitimate use case, and it works out of the box with
tools.deps
.

Let's now create a new file

src/app/jwt.clj
, and use the following code:

(ns app.jwt (:require [no.nsd.clj-jwt :as clj-jwt]
                      [io.pedestal.interceptor.helpers :as interceptor]))
 
(defn- unauthorized [text]
 {:status  401
  :headers {}
  :body    text}) ; Helper to return a 401 status code
 
(defn decode-jwt [{:keys [required? jwk-endpoint]}]
 (interceptor/before
  ::decode-jwt
  (fn [ctx] (if-let [auth-header (get-in ctx [:request :headers "authorization"])]
              (try (->> auth-header
                        (clj-jwt/unsign jwk-endpoint)
                        (assoc-in ctx [:request :claims]))
 
                   (catch Exception _
                     (assoc ctx :response (unauthorized "The token provided is not valid"))))
 
              (if required? (assoc ctx :response (unauthorized "Token not provided"))
                  (assoc-in ctx [:request :claims] {}))))))

We can now import and apply our interceptor in our web application by updating core.clj:

(ns app.core (:require [io.pedestal.http :as http]
                       [io.pedestal.interceptor.helpers :as helpers]
                       [app.jwt :refer [decode-jwt]] ;Add the reference to the new class
                       [environ.core :refer [env]]))
                       
...

(def jwk-endpoint "https://application-name.us.auth0.com/.well-known/jwks.json")
 
(def service-map (-> {::http/routes routes
                     ::http/type   :immutant
                     ::http/host   "0.0.0.0"
                     ::http/join?  false
                     ::http/port   (Integer. (or (env :port) 5000))}
                http/default-interceptors
                (update ::http/interceptors into [http/json-body
                                                  (decode-jwt {:required? true
                                                               :jwk-endpoint jwk-endpoint})])))

And now we can try to send some requests. To obtain a valid JWT token, we would theoretically have to complete an oAuth2 flow. For testing purposes, Auth0's UI offers a token for your current username that you can grab directly from the "Test" tab of the API:

Getting test information from the Auth0 Portal

Now, let's send three requests to our API:

  1. One with that JWT added correctly to the headers
  2. One with an invalid JWT added to the headers
  3. One with no JWT or additional headers at all

The first should be successful, with the remainder being rebuffed with a 401 HTTP status code by our new interceptor.

curl http://localhost:5000/ --header 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ikhsb2NuQ1ZiRFRzdjVMTEs5d2RpeiJ9.eyJpc3MiOiJodHRwczovL3ZuY3oudXMuYXV0aDAuY29tLyIsInN1YiI6IldJeVUybTdZY3BWMHlCeU52dVNHSGpWUEEzYXFRbnA5QGNsaWVudHMiLCJhdWQiOiIvZGV2L251bGwiLCJpYXQiOjE2MzE4Mjk3NTQsImV4cCI6MTYzMTkxNjE1NCwiYXpwIjoiV0l5VTJtN1ljcFYweUJ5TnZ1U0dIalZQQTNhcVFucDkiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.tvzoDnqE5GWfqUKl0XbFtJBv_E3704qDoMOb9YheRj-409ntIcLs02UKVbR1g28-fDEPcQ-IxGRTBcOPEYy7X0HB-IByPCDink_Kj-hJ5O_2ft9I_lYZFOmeRL7TqD3TF9S17hta5UAFEj9zxQ_c_gcI7VY0F3t7iMfe3pNu9ut1l9-iYqjJ_BNCf-pxfQpWqnfQCnjU1qStGHPRmaK1dzXFIdXnH8S0rixcsuaxt5sbFivCIuQTNZ8QxvsW-vJekW1T2IkFOlGRWWWUoKTW2j4PnFlMWnCm7O91xR0zEaAHdQkmAob323qJf9IgucGxD3emX1k_ugLHdfnpvELaEQ'
< HTTP/1.1 200 OK
 
curl http://localhost:5000/ --header 'authorization: Bearer thisIsInvalid'
< HTTP/1.1 401 Unauthorized
 
curl http://localhost:5000/
< HTTP/1.1 401 Unauthorized

Our interceptor is now checking out all the requests and ensuring a valid token is included before moving on.

To give more detail about how JWTs work, we can also create a new route to return the claims encoded in the token to the sender. Update core.clj again:

…
…
(defn get-claims [req] {:status 200 :body (:claims req)}) ; Add a new route returning the claims on the request object
 
(def routes #{["/heroes/:hero" :get get-hero :route-name :get-hero]
             ["/heroes" :get get-heroes :route-name :get-heroes]
             ["/claims" :get get-claims :route-name :get-claims]
             ["/" :get hello-world :route-name :hello-world]})
curl http://localhost:5000/claims --header 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ikhsb2NuQ1ZiRFRzdjVMTEs5d2RpeiJ9.eyJpc3MiOiJodHRwczovL3ZuY3oudXMuYXV0aDAuY29tLyIsInN1YiI6IldJeVUybTdZY3BWMHlCeU52dVNHSGpWUEEzYXFRbnA5QGNsaWVudHMiLCJhdWQiOiIvZGV2L251bGwiLCJpYXQiOjE2MzE4Mjk3NTQsImV4cCI6MTYzMTkxNjE1NCwiYXpwIjoiV0l5VTJtN1ljcFYweUJ5TnZ1U0dIalZQQTNhcVFucDkiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.tvzoDnqE5GWfqUKl0XbFtJBv_E3704qDoMOb9YheRj-409ntIcLs02UKVbR1g28-fDEPcQ-IxGRTBcOPEYy7X0HB-IByPCDink_Kj-hJ5O_2ft9I_lYZFOmeRL7TqD3TF9S17hta5UAFEj9zxQ_c_gcI7VY0F3t7iMfe3pNu9ut1l9-iYqjJ_BNCf-pxfQpWqnfQCnjU1qStGHPRmaK1dzXFIdXnH8S0rixcsuaxt5sbFivCIuQTNZ8QxvsW-vJekW1T2IkFOlGRWWWUoKTW2j4PnFlMWnCm7O91xR0zEaAHdQkmAob323qJf9IgucGxD3emX1k_ugLHdfnpvELaEQ' -v
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=UTF-8
 
{"iss":"https://dev-q6rh8--8.us.auth0.com/","sub":"WIyU2m7YcpV0yByNvuSGHjVPA3aqQnp9@clients","aud":"clojure-pedestal","iat":1631829754,"exp":1631916154,"azp":"WIyU2m7YcpV0yByNvuSGHjVPA3aqQnp9","gty":"client-credentials"}

Conclusions

Building your own Authentication handler isn't an everyday activity in modern web development. Many devs will go their entire career without having to configure JWKS URLs or decoding claims in a token - instead, they'll plug in a library with nice documentation and hope that it works.

Clojure and Pedastal let you take a much better look under the hood at this essential process. Even better, the language and framework make this process incredibly easy to show in a tiny Web API project.

To get more information about Pedestal, its roadmap, and documentation, you can refer to the official website.

We'll try to see something more complicated in the next installment, such as database interactions with Datomic

Using Auth0 Java Libraries

For reference, this is what implementing the same JWT decoder/verifier would look like using the official Auth0 SDK.

(ns app.auth
 (:require [cheshire.core :as json]
           [io.pedestal.interceptor.helpers :as interceptor])
 (:import (java.net URL)
          (com.auth0.jwt JWT)
          (com.auth0.jwt.exceptions SignatureVerificationException AlgorithmMismatchException TokenExpiredException JWTVerificationException JWTDecodeException)
          (com.auth0.jwt.algorithms Algorithm)
          (com.auth0.jwk UrlJwkProvider)
          (com.auth0.jwt.interfaces RSAKeyProvider)
          (org.apache.commons.codec Charsets)
          (org.apache.commons.codec.binary Base64)))
 
(defn- new-jwk-provider
 [url]
 (-> (URL. url)
     UrlJwkProvider.))
 
(def ^{:private true} rsa-key-provider
 (memoize
  (fn [url]
    (let [jwk-provider (new-jwk-provider url)]
      (reify RSAKeyProvider
        (getPublicKeyById [_ key-id]
          (-> (.get jwk-provider key-id)
              (.getPublicKey)))
        (getPrivateKey [_] nil)
        (getPrivateKeyId [_] nil))))))
 
(defn- base64->map
 [base64-str]
 (-> base64-str
     Base64/decodeBase64
     (String. Charsets/UTF_8)
     json/parse-string))
 
(defn- decode-token
 [algorithm token {:keys [issuer leeway-seconds]}]
 (let [add-issuer #(if issuer
                     (.withIssuer % (into-array String [issuer]))
                     %)]
   (-> algorithm
       (JWT/require)
       (.acceptLeeway (or leeway-seconds 0))
       add-issuer
       .build
       (.verify token)
       .getPayload
       base64->map)))
 
(defn- decode [token {:keys [jwk-endpoint] :as opts}]
 (-> (Algorithm/RSA256 (rsa-key-provider jwk-endpoint))
     (decode-token token opts)))
 
(defn- find-token [{:keys [headers]}]
 (some->> headers
          (filter #(.equalsIgnoreCase "authorization" (key %)))
          first
          val
          (re-find #"(?i)^Bearer (.+)$")
          last))
 
(defn- unauthorized [text]
 {:status  401
  :headers {}
  :body    text})
 
(defn decode-jwt [{:keys [required?] :as opts}]
 (interceptor/before
  ::decode-jwt
  (fn [ctx]
    (try
      (if-let [token (find-token (:request ctx))]
        (->> (decode token opts)
             (assoc-in ctx [:request :claims]))
        (if required? (assoc ctx :response (unauthorized "Token not provided"))
            (assoc-in ctx [:request :claims] {})))
 
      (catch JWTDecodeException _
        (assoc ctx :response (unauthorized "The token provided is not valid")))
 
      (catch SignatureVerificationException _
        (assoc ctx :response (unauthorized "Signature could not be verified")))
 
      (catch AlgorithmMismatchException _
        (assoc ctx :response (unauthorized "Algorithm verification problem")))
 
      (catch TokenExpiredException _
        (assoc ctx :response (unauthorized "Token has expired")))
 
      (catch JWTVerificationException _
        (assoc ctx :response (unauthorized "Invalid claims")))))))