Pedestal, Buddy and Security

How to use the buddy-auth libary to secure a Pedestal Application.

Introduction

In this piece I show how to integrate Buddy’s authentication and authorization functionality with a Pedestal web application.

I provide a brief overview of Pedestal’s interceptor model, particularly error-handling, which we’ll use to catch and handle any authentication or authorization errors thrown by Buddy.

I also cover some of Buddy’s available functionality; how to decide if access to a web resource should be allowed; and how to respond to the requesting client when access is denied.

What’s not covered?

For the purpose of this discussion, there is an assumption that the application is using session-based security. However, there is only a short discussion of how the user session is actually populated for use by Pedestal and Buddy.

This is an important topic and if you want to learn more about how to manage the login and session population processes, you can review the following blog post, where I’ve covered it more fully, or the source code for the application, using session based authentication, is available on GitHub (tag v1.0).

The Web Application

The web application will proceed through the following steps:

  • The user requests a resource located at a URL on the server
  • The application identifies & authenticates the user
  • The application determines the valid role(s) for the user
  • The application determines if the user’s role(s) permit access to the resource
  • The application either serves the resource or returns an access-denied response.

Initially, I elide certain details, such as how the user asserts an identity, but return to them later in the piece.

Pedestal

Pedestal is a set of libraries developed by the team at Cognitect to facilitate the creation of web applications in Clojure. It uses the interceptor pattern, which differs from the handler/middleware pattern adopted by Ring.

Oversimplifed, but adequate for our purposes, a Pedestal application is a chain of interceptors with each interceptor being an entity with an :enter function, a :leave function, and an :error function. The web-server receives a request, bundles it into a context (a map) and then threads that context through each interceptor, which has an opportunity to change it, producing a final context which is marshalled into an HTTP response and returned to the requesting client.

The chaining logic through the interceptors is two-pass.

If we consider a chain of interceptors I1, I2 and I3: Pedestal will push the context through I1, then I2 and then I3. It will then pull the context back through the chain in reverse order i.e. I3, then I2, and finally I1. In the push phase each interceptor’s :enter function will be called with the context as its argument, and the return value (also a context) will be passed to the next interceptor’s :enter function. During the pull phase, each interceptor’s :leave function will be called. As with the push phase, each interceptor’s :leave function will receive the context as its argument and is expected to return a context which is passed to the next interceptor.

In the discussion above, I’ve stated that an interceptor function receives a context and returns a context. This is conceptually correct, but obscures one of Pedestal’s useful features.

An interceptor function can also return a core.async channel. When this happens, Pedestal will yield the thread, allowing other activity to occur, and will recommence when a value is available on the channel. When that happens, Pedestal will read one value from the channel (which must be a context) and continue processing the chain.

Although it’s not particularly relevant to our current discussion, it’s important to note that the interceptor chain is not fixed in any general sense. An interceptor, within the context it receives, has access to the chain itself and can manipulate it.

What is relevant is how the Pedestal interceptor chain handles errors and exceptions. Although the Pedestal interceptor call sequence is analagous to a function call stack, the calls do not exist in that nested manner on the JVM call stack. Therefore, it’s not possible to use Clojure’s try/catch mechanism to catch exceptions thrown by one interceptor in a different interceptor further up the stack.

The Pedestal machinery catches any exception thrown by an interceptor, wraps it in an ExceptionInfo instance and associates the instance into the context map with the key :io.pedestal.interceptor.chain/error. Pedestal then back tracks through the interceptor chain looking for an interceptor with an :error function that can handle the exception.

Each :error function is called with two arguments, the context (without the :io.pedestal.interceptor.chain/error key) and the ExceptionInfo instance. If an interceptor’s :error function can handle the error the interceptor should return a context. In that case, Pedestal will continue processing the remaining interceptor’s :leave functions and ultimately return a response to the client.

If an interceptor’s :error function cannot handle the exception it should reattach the ExceptionInfo instance it received to the context (as :io.pedestal.interceptor.chain/error) and return the new updated context. This allows Pedestal to continue searching for an appropriate handler.

During Pedestal’s exception handling process no :enter or :leave functions will be called while the context map contains a :io.pedestal.interceptor.chain/error entry.

Buddy

Buddy is a Clojure library providing authentication and authorization facilites to web applications. Although its documentation is primarily focused on ring based applications, the library itself is flexible enough to be used with just about any Clojure web-application library including Pedestal.

Buddy treats authentication and authorization as independent concerns. Authentication decides who you are, and authorization determines what you can do. I discuss authentication first, and return to the topic of authorization.

Within Buddy there are available many mechanisms to authenticate the user, and Buddy refers to these machanisms as backends. The two we will discuss in greater detail below are session and (in a different blog post) jws (signed JWT).

Conceptually, for all backends, Buddy’s authentication functionality is extremely simple. It occurs in two phases: a parse phase, and an auth phase

Parse Phase

During the parse phase the backend takes the http request (for example, contained in Pedestal’s context map) and extracts from it any values required by the backend’s auth phase. These values could be in the request’s headers, session, query params etc; it depends on the backend. If the parsing of the request returns nothing (nil/false) then further processing by Buddy stops and the auth phase is not entered - the request (and the requestor) is considered unauthenticated. Otherwise, the relevant values from parsing are passed to the auth phase.

Auth Phase

During the auth phase, the values returned by the parse phase are used to determine the identity of the user. This involves calling the backend’s authentication function (auth-fn) with those values. If the auth-fn returns a non-nil, non-false value then the request map’s :identity key is set to that value. This value represents an authenticated user. As with parsing, how authentication is done depends on the backend in use. Possibly, it’s an extraction of a session identifier followed by a database lookup, or even a decryption and verification of a signed JWT that was parsed from the request’s headers.

Buddy provides a number of backend, but you’re also free to define your own if they do not meet your needs.

Review of Application

Considering what we know about interceptors and Buddy, we can now sketch out an approach to securing the application, and then its implementation using Pedestal interceptors.

The Security Model

User

The basic entity is the user. A user can be associated with one or more email addresses. At a later stage this will allow the application use a variety of external identity providers, such as Google, Facebook, Azure etc. Obviously, an email address can be associated with only one application user entity.

Role

A user can also be assigned to one or more roles. Roles determine a user’s permissions within the web application. Roles exist independently of each other and within a flat structure. There are no concepts of hierarchy and inheritance.

Route

Roles are granted (or denied) HTTP verb access to individual uri’s which are represented in the Pedestal world as routes. A route may also be unprotected meaning that its uri is accessible to unauthenticated users (i.e. the public).

There are two ways to allow unprotected access to a resource

  1. Do not use interceptors that manage or restrict access based on identity and permissions. (These interceptors are established for a route with the build-secured-route-vec-to function, which is dicussed in greater detail below).

  2. Name the route using a value in the :alloc-public namespace (see note below on how the permissions lookup table is populated).

Example

The following code fragment defines three routes

(def routes
  (route/expand-routes
    #{["/" :get landing-page :route-name :landing-page]
      ["/api/htest" :get (build-secured-route-vec-to test-response) 
        :route-name :alloc-user/auth-test-response-get]
      ["/api/htest" :post (build-secured-route-vec-to test-response)
       :route-name :alloc-admin/auth-test-response-post]}))

Each vector in the set passed to expand-route, contains a uri pattern, a method, an interceptor (or vector of interceptors) and a :route-name key with its associated value. The application will use the namespace of the route name to build a permission table linking a uri to a role.

In the fragment above the path / is available to any user (including unauthenticated users) as the only active interceptor is the landing-page interceptor, which uses no authentication.

The ability to GET from /api/htest is restricted to users with the :user role, and the ability to POST to the uri is restricted to users with the :admin role. This is ensured because the build-secured-route-vec-to function inserts the necessary authentication and permission checking interceptors into the uri’s interceptor chain before the test-response handler/interceptor.

This approach is helpful as Pedestal, when seeing a pure handler function as the last entry in an interceptor vector, will convert it to an interceptor. (A pure handler function is a single arity function taking a request as its argument). This means that a handler function can be fully exercised in the REPL before attempting to secure it.

Using the information encoded in the routes, the permission table will be constructed by the application at runtime and is used by the interceptor responsible for checking permissions that the authenticated user is in a role required to access the resource.

The Log In Process

In general, and elliding how claims are actually made, when the user attempts to log in the following sequence of events occurs on the server,

  1. The user asserts a claim that they are a valid user with a particular email address

  2. If the server finds this claim to be valid, it will create an identity-token map with the following keys

    Name Explanation
    :alloc-auth/user-id The user’s id e.g. :admin or :act-user
    :alloc-auth/token-type The assertion type made by the user (local or google) and verified by the server
    :alloc-auth/token The user’s token. This will contain fields :email, :iss, :aud, :iat and :exp, which are the user’s email address, the token’s issuer, an audience indicator, the time of the token’s issuance, and the time of its expiration.
    :alloc-auth/ext-token A jwt token created and signed by the server. It contains the user’s email address and can be used by the client to assert an identity independently of the Pedestal interceptor chain. This may be necessary when a session isn’t available i.e. a client can make a request to an api endpoint by including this token in the request’s headers, or when a request to an endpoint outside the security context of Pedestal’s interceptor chain is made e.g. a websocket connection, which is made at the Jetty Session level.
    :alloc-auth/user-session A keyword that is a combination of the user’s id and a monotonically increasing sequence number (e.g. :admin-8 for user :admin). Each Pedestal (Ring) session will be associated with a unique user-session.
  3. The server then adds the identity-token to its record of logged in users, stored in the server.auth.data/alloc-auth-logged-in-users atom.

    This atom retains a list of all the currently logged in users. It is a map where the key is a vector combining the user-id the user-session and a text flag, that is currently always the string “single-session-only”. The value stored in this map is the identity-token.

  4. The server will also write the identity-token to the Ring session’s :identity field

  5. The server will send the Ring session’s identifier as a secure http-only cookie in a transit response to the client.

This completes the initial client/server login and authentication process.

The Interceptor Chain

Now, a consideration of the interceptor chain built by the build-secured-route-vec-to function. This function taking as its first argument an interceptor (or handler function) will return a vector of interceptors appropriate for the dual functions of authenticating the user and authorizing his/her access to the resource (uri). The function also accepts a number of other options, which we will return to later.

  • The first interceptor in the security-related portion of the interceptor chain attempts to authenticate the user. It is provided a context map, and will update the :request portion of the context map using the buddy.auth.middleware/authentication-request function. This function takes as parameters the request, and the backend. It will populate the :identity key of the request map if authentication suceeds (as determined by the backend). If the authentication fails the backend’s :unauthorized-handler is called. This returns a 401 response to the client.

  • The next interceptor retrieves the :identity from the the context’s :request map and looks up the roles associated with the user. It attaches the information retrieved to the context map using the key :alloc-auth/auth. The value added will be a map with two keys :user and :roles. By attaching this information to the context map, it becomes available for interceptors later in the chain.

  • Then the error catching interceptor (created by the function alloc-auth-unauthorized-interceptor) is entered. It returns the received context map unchanged. It’s only responsibility is to handle exceptions that might be thrown later by the interceptors named :alloc-auth-permission-checker and :alloc-auth-access-rule-checker. These two interceptors in turn check the user’s access to a resource (uri) based on his/her assigned roles; and checks his/her access based on custom defined rules. (I provide a expanded description of both below.)

    A benefit of using a single error catching interceptor is that there is a consolidation of application responses in a single area of the code rather than having them spread throughout the code in other interceptors' :error functions. The interceptor is created by the alloc-auth-unauthorized-interceptor function which will select the appropriate backend at the time an authentication or authorization error is encountered. Therefore, in addition to the consistency of responses mentioned above, this approach ensures that backends are fully swappable and can be changed without impacting other areas of the code.

Access Rules

Access rules are helpful when an application developer want to allow access to a route only under certain circumstances; circumstances that cannot be encoded in a route’s uri pattern. As a trivial example, consider the situation where the developer wants to grant access to a uri pattern /api/dostuff/:id between 9:00AM and 5:00PM only.

A way to achieve this is to use an access rule defined according to the convention required by buddy.auth.accessrules.

Such a rule can be expressed as follows

(def rule-1
  [{:uri "/api/dostuff/:id"
    :handler
         (fn [request]
           (let
             [d (time/local-date) n (time/local-date-time)]
             (if (time/before?
               (time/local-date-time (str d "T09:00:00"))
               n
               (time/local-date-time (str d "T17:00:00")))
                 (buddy.auth.accessrules/success)
                 (buddy.auth.accessrules/error))))}])

If it was required that the role should be granted access during those hours only, and when the :id parameter is equal to “company1” the rule would be

(def rule1
  [{:uri "/api/dostuff/:id"
    :handler
         (fn [request]
           (let
             [{company-id :id}
              (-> request :match-params)
              user-identity (-> request :identity)
              auth? (buddy.auth/authenticated? request)
              uri (-> request :uri)
              d (time/local-date)
              n (time/local-date-time)]
             (if (and
                   (= company-id "company1")
                   (time/before?
                     (time/local-date-time (str d "T09:00:00"))
                     n
                     (time/local-date-time (str d "T17:00:00"))))
               (buddy.auth.accessrules/success)
               (buddy.auth.accessrules/error))))}])

Because, buddy-auth attaches the user’s identity to the request map in the context map, it can be retrived and used during the processing of an access rule. Also, any path params extracted from the uri will be available in the handler function in the request map’s :path-params field.

When a rule returns error it results in an unauthorized exception being raised by the backend. This exception is caught in the error catching interceptor as before.

Buddy Backends

As previously mentioned, a backend is responsible for providing a function (authfn) that can authenticate a user, a function (unauthorized-handler) responsible for handling authentication and authorization failures, and possibly a function (on-error) to handle errors.

Internally, a backend is an instance of an object that implements two protocols defined in the buddy.auth.protocols namespace, namely IAuthentication and IAuthorization.

The IAuthentication protocol must provide the -parse method, a function to extract any required information from the suplied request; and the -authenticate method, a function to authenticate the user. The -authenticate method will call the authfn function passed when the backend is created in the application.

The IAuthorization protocol must provide the -handle-unauthorized method which will call the unauthorized-handler function with the request map and a metadata argument describing the failure.

Fortunately, buddy-auth comes with a number of built-in backends.

The Session Back-End

One of the back-ends provided by Buddy is session, which relies on ring’s session support. During the parse phase, the request’s session map is inspected for an :identity key. If that key exists it’s passed to the auth phase, which simply sets the request map’s :identity key to that value. It’s really that simple.

If you use sessions there are a number of security implications that you should consider. First, although the complete session information exists only on the server, the session’s identifier is passed back and forth between the client and the server, and despite some of the security mechanisms employed by browsers (and user agents, more generally), and the cookie-based session functionality provided by ring you will need to be careful.

You should only use https. This ensures that the information passed between the client and the server is encryped in transit. Also, cookies should be set to Secure. Also, you should consider strongly the use of HttpOnly and SameSite. OWASP provides some very good information regarding session security, and you should review it.

Using Ring Session Middleware with Pedestal

Because of the fundamental differences between Pedestal’s interceptor model and Ring’s wrapped middleware model, Pedestal provides in its io.pedestal.http.ring-middlewares namespace an ability to adapt a Ring middleware function to an interceptor context. Conveniently, the namespace also provides a function (session) which does this specifically for adapting Ring’s session middleware. We only need to include the interceptor returned by this function in our interceptor chain to make use of Ring sessions in our Pedestal application.

Using Buddy’s session back-end with Pedestal

Buddy provides an implementation of the session back-end in the buddy.auth.backends namespace and it can be instantiated using the buddy.auth.backends/session function. This function can also accept an options map containing :authfn and :unauthorized-handler keys, which if supplied are expected to be functions that handle authentication and what to do when a request is not authorized respectively. If neither is supplied, Buddy will supply sensible defaults.

For our purposes, the default :authfn function will suffice, but because we will later have to handle authorization we will provide our own :unauthorized-handler function.


(def alloc-auth-session-auth-backend
  (auth.backends/session
    {:unauthorized-handler
     (fn unauthorized-handler
       [request metadata]
       (let [{user         :user roles :roles required :required
              user-session :user-session}
             (get-in metadata [:details :request])
             error-message
             (str "NOT AUTHORIZED (SESSION): In unauthenticated handler for "
                  "uri: " (pr-str (:uri request)) ", "
                  "and path-params "
                  (pr-str (:path-params request)) ". "
                  "user: " (pr-str user) ", "
                  " roles: " (pr-str roles) ". "
                  "required: " (pr-str required) ". "
                  "user-session: " (pr-str user-session) ".")]
         (if user-session
           (rlog/with-forward-context
             user-session
             (log/error
               error-message))
           (log/error
             error-message)))
       (cond
         ;; If request is authenticated, raise 403 instead
         ;; of 401 (because user is authenticated but permission
         ;; denied is raised).
         (auth/authenticated? request)
         (-> (ring-response/response
               {:reason
                (str "Authenticated, but not authorized for access to .\n"
                     "Metadata : " (pr-str metadata))})
             (assoc :status 403))
         ;; In other cases, respond with a 401.
         :else
         (let [current-url (:uri request)]
           (->
             (ring-response/response
               {:reason "Unauthorized"})
             (assoc :status 401)
             (ring-response/header "WWW-Authenticate" "tg-auth, type=1")))))}))

The Route and Interceptor Implementations

Building the Interceptor Chain

For each Pedestal route defined in the application, an interceptor chain (a vector of interceptors) is constructed and included in the service map which is passed to io.pedestal.http/start to start the server. The application uses a function build-secured-route-vec-to to return a vector of interceptors that are installed for the route. The vector returned will include a number of common interceptors in addition to the security-related interceptors we’ve been discussing.

The build-secured-route-vec-to function takes as parameters a handler (or interceptor), and potentially two option parameters, :use-headers and :rules. The handler is installed as the last interceptor in the chain, and is expected to provide the business-logic functionality.

If a :rules option is supplied, it is expected to be a map conforming to the format required by buddy.auth.accessrules. The presence of the :rules option will also cause the :alloc-auth-access-rule-checker interceptor to be included in the vector of interceptors returned.

The :use-headers option can be ignored for now. It will be the subject of another blog post discussing how to create a custom backend for Buddy.

The Security Interceptors

Now let’s take a closer look at the implementation details of the security-related interceptors mentioned above.

The :alloc-auth-authenticate interceptor

This interceptor is responsible for the authentication of the user and is created by the alloc-auth-authentication-interceptor function.

(defn alloc-auth-authentication-interceptor
  [backend]
  (interceptor/interceptor
    {:name  ::alloc-auth-authenticate
     :enter (fn [ctx]
              (-> ctx
                  (assoc
                    :auth/backend
                    backend)
                  (update
                    :request
                    auth.middleware/authentication-request
                    backend)))}))

authentication-request is a function which takes a request and a backend and using the backend attempts to parse the request and to authenticate the user. If the user is sucessfully authenticated an :identity key is added to the request map with the value returned by the backend’s authfn function.

The interceptor performs two functions:

  1. it attaches to the context map the authentication backend being used. This makes it available to other interceptors later in the chain, particularly the error catcher interceptor.

  2. it updates and returns the context map with the (potentially) updated request map returned by the call to authentication-request

The :alloc-auth-user-roles interceptor

This interceptor will attach to the context map information about the roles to which the user has been assigned. It is created by calling the alloc-auth-user-roles-interceptor function.

(defn alloc-auth-user-roles-interceptor
  []
  {:name  ::alloc-auth-user-roles
   :enter (fn [ctx]
            (log/info "Assigning roles for identity " 
              (pr-str (get-in ctx [:request :identity])))
            (let
              [{identity-user-id    :alloc-auth/user-id
                identity-token-type :alloc-auth/token-type
                identity-token      :alloc-auth/token}
               (get-in ctx [:request :identity])]
              (assoc
                ctx
                :alloc-auth/auth
                {:user  identity-user-id
                 :roles (alloc-auth-get-roles-for-identity identity-user-id)})))})

This interceptor was discussed quite extensively above, but two items are worth noting. The authenticated user (in the request map’s :identity field) is expected to be identified by a map with the keys :alloc-auth/user-id, :alloc-auth/token-type and :alloc-auth/token. For our current discussion the first of these is the most important, and using buddy’s session backend would have been extracted from the user’s session object. It is the internal application user id for the user e.g. :admin or :fred.

This value is used by alloc-auth-get-roles-for-identity to return a collection of role entities indicating with which roles the user is associated. The interceptor returns an updated context with this information attached in the :alloc-auth/auth key.

The Error Catcher interceptor

This interceptor will catch authentication and authorization errors raised by the :alloc-auth-permission-checker and :alloc-auth-access-rule-checker interceptors (any others are ignored). This interceptor is created by calling the alloc-auth-unauthorized-interceptor function, which internally uses Pedestal’s error-dispatch function to match errors with handlers.

(defn alloc-auth-unauthorized-interceptor
  []
  (letfn
    [(unauthorized-fn[ctx ex]
       (if-let
         [handling-backend (:auth/backend ctx)]
         (assoc
           ctx
           :response
           (.-handle-unauthorized
             handling-backend
             (:request ctx)
             {:details
              {:request (ex-data (ex-cause ex))
               :message (pr-str (ex-message (ex-cause ex)))}}))
         (do
           (log/error "Unauthorized requests, but there is no backend"
                      "installed to handle the exception.")
           (throw "No auth backend found."))))]
    (error-dispatch
      [ctx ex]
      [{:exception-type :clojure.lang.ExceptionInfo 
        :interceptor ::alloc-auth-permission-checker}]
      (try
        (unauthorized-fn ctx ex)
        (catch Exception e
          (assoc ctx ::interceptor.chain/error e)))
      [{:exception-type :clojure.lang.ExceptionInfo 
        :interceptor :alloc-auth-access-rule-checker}]
      (try
        (unauthorized-fn ctx ex)
        (catch Exception e
          (assoc ctx ::interceptor.chain/error e)))
      :else
      (assoc ctx ::interceptor.chain/error ex))))

The function uses Pedestal’s error-dispatch function to create an interceptor that can handle ExceptionInfo exceptions thrown by either the ::alloc-auth-permission-checker or ::alloc-auth-access-rule-checker interceptors.

In the case of either exception, it will call the backend’s -handle-unauthorized method (from the IAuthorization protocol implemented by the backend), which ultimately calls the unauthorized-handler function registered with the backend (see the notes on alloc-auth-session-auth-backend above).

Any errors that cannot be handled, or throw exceptions during handling are reattached to the context map - potentially to be handled by another interceptor’s :error function, or escaping at the top level with a 5xx error being returned to the client.

Note, that the backend instance to be used when signalling an exception is retrieved from the context map. It was added to the context map by the :alloc-auth-authenticate interceptor (see above).

The :alloc-auth-permission-checker interceptor

This interceptor checks whether the user’s roles (embedded in the context map by :alloc-auth-user-roles) allow access to the requested uri. It is created by calling the alloc-auth-permission-checker-interceptor-factory function.

(defn alloc-auth-permission-checker-interceptor-factory
  []
  (interceptor/interceptor
    {:name  ::alloc-auth-permission-checker
     :enter (fn [ctx]
              (log/info
                (str "Checking Identity: "
                     (pr-str 
                       (get-in ctx 
                         [:request :identity :alloc-auth/user-id] 
                          :unauthenticated))
                     " for access to "
                     (pr-str 
                       (get-in ctx [:request :path-info]))
                     " with path params "
                     (pr-str 
                       (get-in ctx [:request :path-params]))
                     " for route name "
                     (pr-str 
                       (get-in ctx [:route :route-name]))
                     " with session "
                     (pr-str 
                       (get-in ctx [:request :session]))))
              (let
                [{req-path :path-info
                  res-path-params :path-params
                  {identity-user-id    :alloc-auth/user-id
                   identity-token-type :alloc-auth/token-type
                   identity-token      :alloc-auth/token
                   user-session :alloc-auth/user-session} :identity}
                 (get-in ctx [:request])
                 {route-name        :route-name route-method :method
                  route-path-re     :path-re route-path-parts :path-parts
                  route-path-params :path-params}
                 (get-in ctx [:route])
                 {user :user roles :roles}
                 (get-in ctx [:alloc-auth/auth])
                 required-roles
                 (get-in 
                   @alloc-auth-permissions 
                   [route-name :permissions :roles])]

                (log/info
                  (str "User Roles: "
                       (pr-str roles)
                       " , required roles "
                       (pr-str required-roles)))

                (if (and
                      (not (contains? required-roles :public))
                      (empty? (clojure.set/intersection
                                roles required-roles)))
                  (throw
                    (ex-info "Alloc-Unauthorized"
                             {:path     req-path :path-params res-path-params
                              :user     user :roles roles :identity identity
                              :required required-roles
                              :user-session user-session}))
                  (update-in
                    ctx
                    [:request]
                    assoc :auth-alloc "ok"))))}))

If the roles associated with the user, and assoc-ed into the context map earlier as :alloc-auth/auth don’t intersect with the roles required for access (stored in the alloc-auth-permissions atom) an ex-info exception is thrown. The exception will be handled by the error catcher interceptor which will return the appropriate response to the client.

The :alloc-auth-access-rule-checker interceptor

If the Pedestal interceptor chain which is built using build-secured-route-vec-to was passed a :rules parameter, this interceptor will run the rules' handler functions to decide whether access to the resource should be granted (returns success) or denied (returns error).

(defn alloc-auth-rules-checker-interceptor-factory
  [rules]
  (interceptor/interceptor
    {:name  :alloc-auth-access-rule-checker
     :enter (fn [context]
              (let
                [request (:request context)
                 policy :allow
                 w-a-rules-fn
                 (auth.accessrules/wrap-access-rules
                   (fn [req] :ok)
                   {:rules rules :policy policy})]
                (w-a-rules-fn request)
                context))}))

The final security-related interceptor uses buddy.auth.accessrules to determine if access should be granted. buddy.auth.accessrules contains a wrap-access-rules function that is helpful in Ring’s middleware context to wrap other Ring handlers. The interceptor uses this functionality by providing a synthetic handler that returns :ok. This works for our purposes, because the implementation of wrap-access-rules when called with a request will throw an exception if rules are violated for that request. This exception will be caught by the error catcher interceptor. If no exception is thrown, the interceptor returns unchanged the context map it received.

Next Steps

Hopefully, if you’ve been looking for guidance on how to integrate Buddy with Pedestal this document has helped. A later post will consider how one might provide a custom backend for Buddy.

Edit this page

clojure pedestal buddy security authentiation authorization
Kieran Owens
Kieran Owens
CTO of Timpson Gray

Experienced Technology Leader with a particular interest in the use of functional languages for building accounting systems.

comments powered by Disqus

Related