Generate csrf token, send it to frontend, put token in header in following request not working

Hello everybody
I have a react frontend and and phoenix backend. The frontend is not served from phoenix.
Now I would like to generate a csrf token in the phoenix backend, send it to the frontend and put it in the request header to access protected resources. Here are the important code snippets:

router.ex

pipeline :frontend do
plug :accepts, [“json”]
plug :fetch_session
plug :protect_from_forgery
plug :put_secure_browser_headers
end

pipeline :frontend_no_csrf do
plug :accepts, [“json”]
end

scope “/users/”, UserBackendWeb do
pipe_through [:frontend_no_csrf]
scope “/v1/” do
post “/csrf”, UserController, :csrf
end
end
scope “/users/”, UserBackendWeb do
pipe_through [:frontend]
scope “/v1/” do
post “/test”, UserController, :test
end
end

user_controller.ex

def csrf(conn, _opts) do
csrf_token = get_csrf_token()
conn
|> put_resp_cookie(“_csrf_token”, csrf_token, sign: false, same_site: “secure”)
|> json(%{_csrf_token: csrf_token})
end

To access now http://localhost:4000/users/v1/test I use postman and the header is correctly set:

x-csrf-token: fHcxKyYgdz9paj0ZIkkKAD4WL3EQGXU808TIEGEf-ZTwo-8KkWY7ZaLE
Content-Type: application/json
User-Agent: PostmanRuntime 7.28.0
Accept: /
Postman-Token: 4ae26e18-87d9-4d61-9bf5-d8e16f726486
Host: localhost:4000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 73
Cookie: _csrf_token=fHcxKyYgdz9paj0ZIkkKAD4WL3EQGXU808TIEGEf-ZTwo-8KkWY7ZaLE

Put I get a 403 back, because the token seems not to be seen?
Converted error Plug.CSRFProtection.InvalidCSRFTokenError to 403 response

Where do I make a mistake?
Best

The point of csrf is to verify two things match, usually a hidden field in the form matches with the session. The csfr token by itself does not prove anything. You’d need to maintain a phoenix session cookie as well.

1 Like

So the actual solution would be to to be store the csrf_token in the session.

def csrf(conn, _opts) do
csrf_token = get_csrf_token()
conn
|> put_session(“_csrf_token”, Process.get(:plug_unmasked_csrf_token))
|> json(%{_csrf_token: csrf_token})

Consequently the token in the x-csrf-token header will be valid.

1 Like

Are you sure you even need CSRF token? If you are creating a JSON only API and verify POST content type is application/json I don’t think there is a way to do an CSRF attack if your CORS headers are properly set.

1 Like

@wanton7 yes, if it would be a JSON only API and no cookies were involved, this would be the solution. But since I don’t want to store the token in the frontend in the local storage, which is prone to xss attacks, I use a session cookie. And whenever cookies are involved for authentication, csrf protection needs to be in place.
Out of convenience I used the Guardian sign_in function, which handles the session.

whenever cookies are involved for authentication, csrf protection needs to be in place.

This is not true. Like I said if you make sure to check POST is content type of application/json because form post can’t be that content type and if your CORS headers are correctly set so no one can AJAX post your site there should be no way to do CSRF attack even with cookie authentication.

1 Like

As an additional note there is a draft that would add application/json content type support for forms W3C HTML JSON form submission
But in that draft application/json would be protected by same origin policy W3C HTML JSON form submission . So it’s a non issue even if accepted.
For extra security if you run your API in same domain as your UI you can also use cookie SameSite attribute.

To summarize:

  • Check all POST/PUT/PATCH request (non GET requests with body) to API that they have content type of application/json
  • Make sure your CORS headers are properly set so that they don’t allow XHR (AJAX) request from other 3rd party domains. So DON’T go and set Access-Control-Allow-Origin to a * (asterisk)
  • Use cookie SameSite for extra protection if your UI and API are in same domain. Default should already be Lax so cookies are basically only sent with GET requests from 3rd party domains. Default Lax should already protect API from CSRF attacks, but better be safe than sorry by setting it manually. More information about it here SameSite cookies - HTTP | MDN
1 Like