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
-
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). -
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,
-
The user asserts a claim that they are a valid user with a particular email address
-
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
orgoogle
) 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 uniqueuser-session
. -
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
theuser-session
and a text flag, that is currently always the string “single-session-only”. The value stored in this map is the identity-token. -
The server will also write the identity-token to the Ring session’s
:identity
field -
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 thebuddy.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 thealloc-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.-
The next interceptor in the chain (
:alloc-auth-permission-checker
) extracts the:alloc-auth/auth
value from the context map and compares its:roles
value against the roles required to access the resource using the permissions table. If the comparison fails to find a match between the user’s assigned roles and the roles required to access the resource, the interceptor will throw an “Alloc-Unauthorized” exception. This will cause Pedestal to start looking for a handler, finding it in the error catching interceptor (see above), which ultimately returns a 401 or 403 response to the client. -
If the request has gotten this far, then the user is superficially allowed to access the resource (by HTTP verb and uri), but there may be other, finer restrictions to be considered. The final interceptor related to security is now entered (
:alloc-auth-access-rule-checker
). It takes the context map’s:request
value and runs a set of rules against it using functions in thebuddy.auth.accessrules
namespace. If these rules result in asuccess
the resource is returned to the client; if not, an exception is thrown, which is again handled by the error catching interceptor. This will return a 401 or 403 response to the client as appropriate.
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 therequest
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 therequest
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, namelyIAuthentication
andIAuthorization
.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 theauthfn
function passed when the backend is created in the application.The
IAuthorization
protocol must provide the-handle-unauthorized
method which will call theunauthorized-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 ofHttpOnly
andSameSite
. 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 thebuddy.auth.backends
namespace and it can be instantiated using thebuddy.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 functionbuild-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 bybuddy.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
interceptorThis 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’sauthfn
function.The interceptor performs two functions:
-
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.
-
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
interceptorThis 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 thealloc-auth-unauthorized-interceptor
function, which internally uses Pedestal’serror-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 handleExceptionInfo
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 theIAuthorization
protocol implemented by the backend), which ultimately calls theunauthorized-handler
function registered with the backend (see the notes onalloc-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
interceptorThis 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 thealloc-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 thealloc-auth-permissions
atom) anex-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
interceptorIf 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 (returnssuccess
) or denied (returnserror
).(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 awrap-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 ofwrap-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.
-