Compojure routes with different middleware

user1391110 picture user1391110 · May 30, 2012 · Viewed 9.3k times · Source

I'm currently writing an API in Clojure using Compojure (and Ring and associated middleware).

I'm trying to apply different authentication code depending on the route. Consider the following code:

(defroutes public-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT")))

(defroutes user-routes
  (GET "/user-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "/user-endpoint2" [] ("USER ENDPOINT 1")))

(defroutes admin-routes
  (GET "/admin-endpoint" [] ("ADMIN ENDPOINT")))

(def app
  (handler/api
    (routes
      public-routes
      (-> user-routes
          (wrap-basic-authentication user-auth?)))))
      (-> admin-routes
          (wrap-basic-authentication admin-auth?)))))

This doesn't work as expected because wrap-basic-authentication indeed wraps routes so it gets tried regardless of the wrapped routes. Specifically, if the requests needs to be routed to admin-routes, user-auth? will still be tried (and fail).

I resorted to use context to root some routes under a common base path but it's quite a constraint (the code below may not work it's simply to illustrate the idea):

(defroutes user-routes
  (GET "-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "-endpoint2" [] ("USER ENDPOINT 1")))

(defroutes admin-routes
  (GET "-endpoint" [] ("ADMIN ENDPOINT")))

(def app
  (handler/api
    (routes
      public-routes
      (context "/user" []
        (-> user-routes
            (wrap-basic-authentication user-auth?)))
      (context "/admin" []
        (-> admin-routes
            (wrap-basic-authentication admin-auth?))))))

I'm wondering if I'm missing something or if there's any way at all to achieve what I want without constraint on my defroutes and without using a common base path (as ideally, there would be none).

Answer

vedang picture vedang · May 30, 2012
(defroutes user-routes*
  (GET "-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "-endpoint2" [] ("USER ENDPOINT 1")))

(def user-routes
     (-> #'user-routes*
         (wrap-basic-authentication user-auth?)))

(defroutes admin-routes*
  (GET "-endpoint" [] ("ADMIN ENDPOINT")))


(def admin-routes
     (-> #'admin-routes*
         (wrap-basic-authentication admin-auth?)))

(defroutes main-routes
  (ANY "*" [] admin-routes)
  (ANY "*" [] user-routes)

This will run the incoming request first through admin-routes and then through user routes, applying the correct authentication in both cases. The main idea here is that your authentication function should return nil if the route is not accessible to the caller instead of throwing an error. This way admin-routes will return nil if a) the route actually does not match defined admin-routes or b) the user does not have the required authentication. If admin-routes returns nil, user-routes will be tried by compojure.

Hope this helps.

EDIT: I wrote a post about Compojure some time back, which you might find useful: https://vedang.me/techlog/2012-02-23-composability-and-compojure/