Kit: Apply Middleware to Some Routes

I’m trying to implement a REST API using Kit. My routes are defined as follows:

(defn api-routes [_opts]
   ["/auth/login" {:post auth/login}]
   ["/auth/checked" {:get (wrap-auth auth/checked)}]
   ["/auth/open" {:get auth/open}]])

The /auth/login endpoint returns a signed JWT, which works fine. Now I’d like the endpoint /auth/checked to run through authenticaton, while /auth/open remains open. The endpoints don’t do anything meaningful:

(defn checked [_req] {:status 200 :body "require auth"})
(defn open [_req] {:status 200 :body "without auth"})

More important is the wrap-auth function, defined as follows:

(defn wrap-auth [handler]
  (let [backend (backends/jws {:secret "topsecret"})]
    (-> handler
        (auth-middleware/wrap-authentication backend)
        (auth-middleware/wrap-authorization backend))))

With backend coming from buddy.auth.backends.

However, both endpoints work without any Authorization header.

I’m totally lost how middlewares are supposed to be applied to some routes but not to others. Any ideas how to do this?

Is there any Kit demo application that makes use of JWT bearer authentication?

I’m looking at the Kit documentation on how to restrict access but don’t understand anything.

I just answered to your SO question that asks the same.

1 Like

Thanks!

Just to make sure that I understand it correctly: The handler code is executed in any case (authenticated or not authenticated), but for authenticated request, the :identity key is available in the request?

Indeed. So if it’s missing, the auth has failed.

1 Like

I must be doing something totally wrong. I now setup a self-contained example project: kit-jwt-auth, which links to the full code:

Routing:

(defn api-routes [_opts]
  [["/login" {:post endpoints/login}]
   ["/open" {:get endpoints/open}]
   ["/closed" {:get (wrap-auth endpoints/closed)}]])

Endpoints:

(def accounts [{:username "alice" :password "topsecret"}
               {:username "bob" :password "mostsecret"}])

(defn auth? [username password]
  (let [account (first (filter #(= (:username %) username) accounts))]
    (and account (= (:password account) password))))

(defn login [req]
  (let [body (:body-params req)
        username (:username body)
        password (:password body)]
    (if (auth? username password)
      (let [iat (quot (System/currentTimeMillis) 1000)
            exp (+ iat (* 24 60 60))
            claims {:sub username :iat iat :exp exp}
            token (jwt/sign claims token-secret {:alg :hs512})]
        {:status 200 :body {:token token}})
      {:status 401})))

(defn closed [req]
  (if (:identity req)
    {:status 200 :body "auth successful\n"}
    {:status 403 :body "unauthorized\n"}))

(defn open [_req]
  {:status 200 :body "no auth required\n"})

Middleware:

(defn wrap-auth [handler]
  (let [backend (backends/jws {:secret token-secret :token-name "Bearer"})]
    (auth-middleware/wrap-authentication handler backend)))

I can get a token:

curl -X POST -H 'Content-Type: application/json' -d '{"username": "alice", "password": "topsecret"}' http://localhost:3000/api/login | jq -r ' .token' > token.txt

And I can validate it on jwt.io successfully.

However, authentication still fails:

curl -H "Authorization: Bearer $(cat token.txt)" http://localhost:3000/api/closed

Returns: unauthorized

Any ideas what I’m doing/understanding wrong?

Re-writing the wrap-auth function in terms of primitives does work, as my colleague figured out:

(ns com.github.patrickbucher.kit-jwt-auth.web.middleware.auth
  (:require
   [buddy.sign.jwt :as jwt]
   [com.github.patrickbucher.kit-jwt-auth.config :refer [token-secret]]))

(defn wrap-auth [handler]
  (fn [req]
    (let [auth-header (get-in req [:headers "authorization"])]
      (if (and auth-header (.startsWith auth-header "Bearer "))
        (let [token (subs auth-header 7)]
          (try
            (let [claims (jwt/unsign token token-secret {:alg :hs512})
                  identity (:sub claims)]
              (println "JWT claims:" claims)
              (handler (assoc req :identity identity)))
            (catch Exception e
              (println "Token error:" (.getMessage e))
              {:status 401 :body {:error "invalid token"}})))
        (do
          (println "Missing token")
          {:status 401 :body {:error "missing token"}})))))

So there must be an issue how I’m using the backend.