This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-01-07
Channels
- # announcements (2)
- # beginners (30)
- # calva (7)
- # cherry (15)
- # clerk (21)
- # clojure (1)
- # clojure-losangeles (12)
- # clojure-norway (1)
- # clojure-spec (3)
- # clojurescript (31)
- # conjure (2)
- # cursive (44)
- # datomic (13)
- # emacs (13)
- # honeysql (15)
- # hyperfiddle (7)
- # malli (2)
- # off-topic (17)
- # overtone (6)
- # reitit (7)
- # ring (58)
- # shadow-cljs (12)
- # squint (14)
- # tools-deps (14)
- # web-security (1)
- # xtdb (29)
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.
where does ring.middleware.anti-forgery actually store the token in-order to validate it?
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.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.tried configuring wrap-session to use encrypted cookie. it does add the cookie but my session map is still empty
Can you post your full middleware? It's possible that you are unknowingly adding wrap-session
twice.
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.
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.What happen if you put wrap-cookies
after wrap-session
?
I still get "Invalid anti-forgery token". I'll inspect what's happening.
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
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.
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.
So when ordering middleware, the order is backward to how you might initially think of it.
ah good catch on the cookie-store. still doesn't work, unfortunately
seems to me that the middleware at the top is more "outside" since it is running first
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.
Like you'd assume that f
would be applied first, followed by g
, followed by h
.
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.
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.
Could you show me the full middleware you have now, just to check it all looks okay after the changes that were made?
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
my-cookie-store defined in a let outside my-cookie-store (cookie-store)
And the anti-forgery token the println
outputs is correct?
And this is the only middleware you have? There's no other middleware (like wrap-defaults) anywhere on the outside of your routes?
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
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)
yes I was using a middleware like that at various points, but I redacted it to make it less messy
however with things in this order they don't run for session or cookie middlewares as never gets that far
(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.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))
so I can customize the identifier to see which one it is
and just calling diff from dired to see what changes each time
it filters out calls to favicon etc also
So what does the session look like when the anti-forgery middleware gets to it? And what does it look like after?
You'll also want to check the response map as well as the request map in your debug middleware.
these lines are added:
+ :anti-forgery-token
+ "UrG7+bK9KERrJ2aa6Rj+CUHkTHYpPeH/5QSDN7SQooNzJ4Zw+ADZkxlYkiNWilNVMKp806xVv4ur+ohv",
I assume that's to the request session?
nope at the top level. :session doesn't exist yet
Okay, that's interesting...
So there's no :session
key on the request map at all?
no but when the wrap-session middleware runs next it adds an empty map :session {}
also adds cookie
+ :cookies
+ {"JSESSIONID" {:value "VxaXMwsUmqHPPuIfuYZAV_WtBb56Af2TIipOnibT"},
+ "ring-session"
+ {:value
+ "6BBXoc6/qqVgo5UwOgR8R5MCQD/peI5H0Oax4B4u1pr8+5KwiHoiyETjFJ9BZ4wqhSkxlXmXMGRHX5nv+y7RynvRAz0voStojSwrnwp0XYfKUMr6A484uHgSZ6yHghYzkzfYx/7KvI+IM4SJDL8MDrKqVoBuWaAbOiUrHdNf+ulmmJS1w6Ng+fjEV0qph+qqx0ucj4v7jTMHZwtcCI2ajQ==--UmS5KnCPRXPuSMJbErD0xJnbpZBmhzhpP2HUf930y2Q="}},
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 ?
(Incidentally, this might be a question for the #C7YF1SBT3 group!)
ah that looks better. now :session contains :ring.middleware.anti-forgery/anti-forgery-token
That is very strange! I wonder why they chose to iterate middleware in reverse like that.
wrap-session put it there though, not wrap-anti-forgery
so wrap-session is getting it from the cookie I presume
anyway I'm still getting Invalid anti-forgery token error
when I post
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?
I wonder if there is some nuance about reitit's "data-driven-middleware" that's causing a problem.
yep I'll create a sample project
You may want to ask in the #C7YF1SBT3 group as well.
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