Fork me on GitHub
#ring
<
2024-01-07
>
Joseph Graham17:01:22

Hi all. I'm finding I can't get ring.middleware.anti-forgery working. It always returns the "Invalid anti-forgery token" page. I have confirmed it is able to retrieve the token from the form field, so assume the problem is with the session. In the example in https://github.com/ring-clojure/ring-anti-forgery/tree/master, wrap-session is after wrap-anti-forgery. I don't understand that as how could it save any info in the session if the session isn't there yet? Of-course I tried swapping them around but that did not help either.

Joseph Graham17:01:18

where does ring.middleware.anti-forgery actually store the token in-order to validate it?

Joseph Graham06:01:20

in the source code where it checks the token is valid it tries to retrieve it from the session

(get-in request [:session :ring.middleware.anti-forgery/anti-forgery-token])
But my :session is just an empty map.

Joseph Graham06:01:53

wrap-cookies middleware adds this:

:cookies {"JSESSIONID" {:value "VxaXMwsUmqHPPuIfuYZAV_WtBb56Af2TIipOnibT"}, "ring-session" {:value "b7880cb4-2348-42e1-8353-e142601476bd"}},
Then wrap-session adds:
:session/key nil,
:session {}
Then wrap-anti-forgery shows the Invalid anti-forgery token error.

Joseph Graham06:01:19

tried configuring wrap-session to use encrypted cookie. it does add the cookie but my session map is still empty

weavejester12:01:57

Can you post your full middleware? It's possible that you are unknowingly adding wrap-session twice.

weavejester13:01:59

wrap-session needs to be after wrap-anti-forgery because it's needs to be on the "outside" and the anti-forgery middleware on the "inside". Don't think of it as a sequence of operations, but as a sequence of "wrappings" or "layers". By having wrap-session on the outside, it's able to add the :session key to the request, and process the :session key on the response.

Joseph Graham13:01:31

thanks. I have tried them both ways round but no luck. But here is how it looks rn (using reitit):

{:data {
              :coercion   reitit.coercion.spec/coercion
              :muuntaja   m/instance
              :middleware [logger/wrap-log-response
                           parameters/parameters-middleware
                           rrc/coerce-request-middleware
                           muuntaja/format-response-middleware
                           rrc/coerce-response-middleware
                           logger/wrap-log-request-start
                           wrap-query-builder
                           wrap-cookies
                           (fn [x] (wrap-anti-forgery x {:read-token
                                                         (fn [{{{:keys [__anti-forgery-token]} :form} :parameters}]
                                                           (println (str "token: " __anti-forgery-token))
                                                           
                                                           __anti-forgery-token)}))
                           (fn [x] (wrap-session x {:store (cookie-store)}))]}}
The function wrapping wrap-anti-forgery is just so I can confirm it's successfully getting the token.

weavejester13:01:42

What happen if you put wrap-cookies after wrap-session?

Joseph Graham13:01:44

I still get "Invalid anti-forgery token". I'll inspect what's happening.

Joseph Graham13:01:10

the problem is this way round, the debug middleware's I'm using (not shown, just spits the request map to a file) don't get very far since wrap-anti-forgery intercepts it

weavejester13:01:52

Ah, I've spotted another potential issue: try taking (cookie-store) out into a let clause or def. If it's being defined multiple times, you'll have multiple cookie stores each with a random key.

weavejester13:01:58

Because middleware are wrappers, they go from "inside" to "outside", as if you were putting layers of wrapping paper around a gift. Middleware that's "inside" can access keys that middleware that's more "outside" add the the request.

weavejester13:01:49

So when ordering middleware, the order is backward to how you might initially think of it.

Joseph Graham13:01:33

ah good catch on the cookie-store. still doesn't work, unfortunately

Joseph Graham13:01:16

seems to me that the middleware at the top is more "outside" since it is running first

weavejester13:01:49

Hm. Let me check how reitit applies middleware. I'm assuming that [f g h] applies as (-> f g h), but perhaps that's backward.

weavejester13:01:36

Like you'd assume that f would be applied first, followed by g, followed by h.

weavejester13:01:26

The docs don't say, but I assume the middleware is applied in order, from first to last, rather than backward, from last to first.

weavejester13:01:54

If the middleware is applied from first to last, the first middleware would be the innermost middleware, and the last middleware would be the outermost.

weavejester13:01:17

Could you show me the full middleware you have now, just to check it all looks okay after the changes that were made?

Joseph Graham13:01:01

logger/wrap-log-response
parameters/parameters-middleware
rrc/coerce-request-middleware
muuntaja/format-response-middleware
rrc/coerce-response-middleware
logger/wrap-log-request-start
wrap-query-builder

(fn [x] (wrap-anti-forgery x {:read-token
                              (fn [{{{:keys [__anti-forgery-token]} :form} :parameters}]
                                (println (str "token: " __anti-forgery-token))
                                                           
                                __anti-forgery-token)}))
(fn [x] (wrap-session x {:store my-cookie-store}))
wrap-cookies

Joseph Graham13:01:13

my-cookie-store defined in a let outside my-cookie-store (cookie-store)

weavejester13:01:34

And the anti-forgery token the println outputs is correct?

weavejester13:01:53

And this is the only middleware you have? There's no other middleware (like wrap-defaults) anywhere on the outside of your routes?

weavejester13:01:58

You could also try adding debug middleware like: (fn [h] (fn [r] (prn (select-keys r [:session :cookies :parameters])) (let [rsp (h r)] (prn (select-keys rsp [:session :cookies :parameters])) rsp))) at various points inside the middleware chain to peek at what the session, cookies and parameters are set to

Joseph Graham13:01:12

the anti-forgery token from println matches what was in the HTML hidden form input in the previous page-load. And this is put there by extracting it from (:anti-forgery-token request)

Joseph Graham13:01:12

yes I was using a middleware like that at various points, but I redacted it to make it less messy

Joseph Graham13:01:53

however with things in this order they don't run for session or cookie middlewares as never gets that far

weavejester13:01:59

(defn wrap-debug-info [handler]
  (fn [request]
    (prn :request (select-keys request [:cookies :session :parameters]))
    (let [response (handler request)]
      (prn :response (select-keys response [:cookies :session :parameters]))
      response)))
That should be a better debug middleware.

Joseph Graham13:01:17

here is my debug middleware:

(defn wrap-save-reqmap-to-file
  "we save the requestmap to a file so we can analyze"
  [comment handler]
  (fn [req]
    (when (string/includes? (:uri req) "/question/")
      (let [timestamp (.toString (java.time.LocalDateTime/now))
            filename (str "reqlog/request-" comment "-" timestamp ".edn")]
        (clojure.pprint/pprint req ( filename))))

    ; Call the handler and return its response
    (handler req)))
and I was inserting it like this (fn [x] (wrap-save-reqmap-to-file "identifier" x))

Joseph Graham13:01:33

so I can customize the identifier to see which one it is

Joseph Graham13:01:12

and just calling diff from dired to see what changes each time

Joseph Graham13:01:29

it filters out calls to favicon etc also

weavejester13:01:21

So what does the session look like when the anti-forgery middleware gets to it? And what does it look like after?

weavejester13:01:39

You'll also want to check the response map as well as the request map in your debug middleware.

Joseph Graham13:01:25

these lines are added:

+ :anti-forgery-token
+ "UrG7+bK9KERrJ2aa6Rj+CUHkTHYpPeH/5QSDN7SQooNzJ4Zw+ADZkxlYkiNWilNVMKp806xVv4ur+ohv",

weavejester13:01:48

I assume that's to the request session?

Joseph Graham13:01:47

nope at the top level. :session doesn't exist yet

weavejester13:01:06

Okay, that's interesting...

weavejester13:01:49

So there's no :session key on the request map at all?

Joseph Graham13:01:40

no but when the wrap-session middleware runs next it adds an empty map :session {}

Joseph Graham13:01:00

also adds cookie

+ :cookies
+ {"JSESSIONID" {:value "VxaXMwsUmqHPPuIfuYZAV_WtBb56Af2TIipOnibT"},
+  "ring-session"
+  {:value
+   "6BBXoc6/qqVgo5UwOgR8R5MCQD/peI5H0Oax4B4u1pr8+5KwiHoiyETjFJ9BZ4wqhSkxlXmXMGRHX5nv+y7RynvRAz0voStojSwrnwp0XYfKUMr6A484uHgSZ6yHghYzkzfYx/7KvI+IM4SJDL8MDrKqVoBuWaAbOiUrHdNf+ulmmJS1w6Ng+fjEV0qph+qqx0ucj4v7jTMHZwtcCI2ajQ==--UmS5KnCPRXPuSMJbErD0xJnbpZBmhzhpP2HUf930y2Q="}},

weavejester13:01:05

Okay, so does Reitit apply it's middleware backward? From outside in, rather than from inside out? What happens if you reverse the order completely, so cookies -> session -> anti-forgery ?

weavejester13:01:47

(Incidentally, this might be a question for the #C7YF1SBT3 group!)

Joseph Graham13:01:40

ah that looks better. now :session contains :ring.middleware.anti-forgery/anti-forgery-token

weavejester14:01:11

That is very strange! I wonder why they chose to iterate middleware in reverse like that.

Joseph Graham14:01:14

wrap-session put it there though, not wrap-anti-forgery

Joseph Graham14:01:39

so wrap-session is getting it from the cookie I presume

Joseph Graham14:01:02

anyway I'm still getting Invalid anti-forgery token error

weavejester14:01:56

This is somewhat difficult to diagnose because I can't see the full code, and I'm unfamiliar with how Reitit applies middleware. I'm still not entirely convinced it applies middleware in reverse order, but I guess we can easily check that by asking the Reitit folks. Can you create a small sample project that demonstrates the problem?

Joseph Graham14:01:12

I wonder if there is some nuance about reitit's "data-driven-middleware" that's causing a problem.

Joseph Graham14:01:18

yep I'll create a sample project

weavejester14:01:47

You may want to ask in the #C7YF1SBT3 group as well.

Joseph Graham14:01:15

Yep. Many thanks. I'll share a sample project later today or tomorrow.

👍 2
Joseph Graham19:01:10

I did get this working in the end. Few notes: • It is indeed necessary for the session store to be defined outside otherwise it uses a different store for each route • The order of the middleware is indeed "reverse". i.e. I need to put wrap-session above wrap-anti-forgery for this to work. • Turns out my use of a 307 redirect instead of 303 in my implementation of Post/Redirect/Get pattern was causing my request to Post to the redirect location, causing another error even once I fixed it