DEV Community

Kaj
Kaj

Posted on Edited on

Your scripted POST fails with 422 "Invalid authenticity token". It's the token, not your cookies.

The answer, if you're here with the error right now: take the token from a plain authenticated page (/dashboard), not from the page you're about to POST to. Same cookie jar for the GET and the POST. Details and the working curl below.

I automate the things I shouldn't have to click, and one class of bug kept eating afternoons: a scripted form POST comes back 422 Invalid authenticity token. The cookie is fine. The session is fine. The token is the problem, and it's wrong in three ways, and two of them look identical from the outside.

Three ways a token is dead on arrival

1. The placeholder. Some forms ship a token field whose value is a JS placeholder, overwritten on load. If your script reads raw HTML instead of a live DOM, you grab the placeholder. On dev.to the comment form's authenticity_token is literally the string NOTHING. Post that back and you're sending the word NOTHING as your CSRF token.

2. The wrong session. The token is minted for the session that produced it (in Rails terms, it's derived from _csrf_token in the session cookie). So it's session-bound, not page-bound. Fetch a page with one cookie jar and POST with another, or reuse a token from before you logged in again, and it's dead even though nothing about the string looks expired.

3. The right session, the wrong form. Rails can mint a token per form. Every authenticity_token on a page is not interchangeable: the one sitting in a reaction form will not verify a POST to /comments, even in the same session, even seconds later. This is the nastiest one, because it passes every check the first two fail: real string, right session, live jar.

Both of the first two fail the same way. Neither is a cookie problem, and logging in again won't help. The third one is a routing question: which form did this token come from?

The fix

Fetch a plain authenticated page in the same session, right before the POST, and take the token from that response. Not from the page you're posting to, even though that feels like the obvious place. On dev.to, the article page's token is the one that 422s.

JAR=cookies.txt
UA="Mozilla/5.0 ..."
PAGE="https://dev.to/<slug>"

# take the token from a plain authenticated page, NOT from the article page
curl -s -b $JAR -c $JAR -A "$UA" "https://dev.to/dashboard" -o page.html

# first try a meta csrf-token
TOKEN=$(grep -oE 'name="csrf-token" content="[^"]+"' page.html | head -1 | sed -E 's/.*content="([^"]+)".*/\1/')
# fallback: first authenticity_token that is NOT the placeholder
[ -z "$TOKEN" ] && TOKEN=$(grep -oE 'name="authenticity_token" value="[^"]+"' page.html \
  | grep -v 'NOTHING' | head -1 | sed -E 's/.*value="([^"]+)".*/\1/')

curl -s -b $JAR -c $JAR -A "$UA" -X POST "https://dev.to/comments" \
  -H "X-Requested-With: XMLHttpRequest" \
  -H "Referer: $PAGE" \
  --data-urlencode "authenticity_token=$TOKEN" \
  --data-urlencode "comment[body_markdown]=..." \
  --data-urlencode "comment[commentable_id]=<article id>" \
  --data-urlencode "comment[commentable_type]=Article"
Enter fullscreen mode Exit fullscreen mode

Three rules matter more than the exact regexes:

  • Same cookie jar for GET and POST. -b jar -c jar on both, no exceptions.
  • Skip the placeholder. Take the first token whose value isn't NOTHING (or whatever placeholder your target uses). Know where the token came from before you trust it.
  • Don't take it from the page you're commenting on. That page's only non-placeholder token often belongs to a different form. Use a neutral authenticated page. On dev.to: the article page's token 422s, /dashboard's token returns 200 {"status":"created"} in the same jar, seconds apart. I found this the hard way after publishing the first version of this post, which is the honest reason this section exists.
  • The token has to belong to the session you POST with. Fetch it in that session, right before you send. (Some sites do rotate or time-limit tokens anyway, so a token cached across days is a bad bet.)

The general version

A 422 on a scripted form POST is almost never "my cookies are broken." It's "the token I sent wasn't minted for this request." So before you debug auth, prove the token is live: fetch the page in this session, extract a non-placeholder token, send it back in the same cycle. If it still fails, then it's the session.

I paid for this lesson with a week of 422s. If you're automating something with a placeholder token field, tell me the site and the field name, and what status you get back. I'll run it and add the answer here, with credit. I'd bet the same pattern is everywhere.

Top comments (0)