Deploying a Rails API/React App with Sessions and CSRF Tokens

Megan McCarty
CodeX
Published in
5 min readSep 21, 2021
What I stared at for countless hours

What a learning experience I had in deploying a React/Rails API app! The process was much more difficult than I ever anticipated, and I spent dozens of hours Googling problems. The main issues I encountered involved properly structuring and configuring my project to work with Rails sessions and CSRF tokens.

My problems all started with the innocent idea of deploying my backend and frontend separately…

Issue #1: Project Structure

So, I decided to deploy my Rails API to Heroku and my React frontend to Netlify. After all, having the backend and frontend running locally on different ports worked just fine, so why should deploying to separate hosting providers be any different?

Well, as it turns out, it’s actually quite different, especially when user authentication is involved! After deployment, I discovered that Rails sessions stopped working entirely, which broke my app. Users could “log in”, but if they refreshed the page, they’d immediately be logged out. And users could not make successful POST requests to one of my resources, because the session_id was empty.

Why was this? After some initial Googling, it turns out that Rails stores the session_id in a cookie, and cookies are not transferred across domains. Meaning, if I wanted to use sessions, my frontend and backend would need to be deployed together, not on separate hosting platforms.

Rather than restructure my repos, I ran npm run build inside my frontend directory and copied the contents of the resulting /build folder over to my backend’s /public directory. Because I used React Router in my project, I had to tweak a bit of code inside my application_controller.rb file and in my config/routes.rb file for Rails to render the index.html generated by React (kudos to Charlie Gleason over at Heroku for writing a phenomenal tutorial, it greatly helped me here!)

# application_controller.rb# NOTE: ActionController needs to inherit from ::Base, NOT ::API, in order to render the index.html file:
# https://stackoverflow.com/a/59387177/11860889
class ApplicationController < ActionController::Base
include ActionController::Cookies
def fallback_index_html
render file: 'public/index.html'
end
end
# config/routes.rb...get '*path', to: "application#fallback_index_html", constraints: ->(request) do
!request.xhr? && request.format.html?
end

Ok, perfect! Now, I was ready to redeploy this repo to Heroku!

Or was I?

Issue #2: Can’t Verify CSRF Token Authenticity

Having redeployed my project as one repo on Heroku, I then ran into an issue where I could not perform any kind of non-GET request. Instead, I was greeted with a lovely 422 Unprocessable Entity error, with the details being:

Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 1ms (Active Record: 0.0s)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken)

Because my ActionController was now inheriting from ::Base, Rails was expecting any non-GET request to contain a CSRF token inside the header.

I found a quick “fix” that entailed disabling CSRF protection by adding the following line to my application_controller.rb file:

skip_before_action :verify_authenticity_token

My app now worked like it did locally! But I wasn’t satisfied, as I thought it was lazy to disable security to make my app work. So back to Googling!

A bit of background on CSRF

CSRF stands for Cross-Site Request Forgery, a type of security attack commonly known as a “one-click attack”. For example, let’s say you have an account on a website without CSRF protection, e.g., www.my-bank.com. You log in, and a cookie with your session_id is stored in your browser, which keeps you logged in as you browse the site. Then, without logging out, you happen to visit a malicious website that has some hidden code; this code might be triggered when clicking a link or when the page simply loads. The embedded code makes a non-GET request to www.my-bank.com (like changing your email address or transferring your money out of your account), and, because the cookie containing your session_id for www.my-bank.com is still in your browser, www.my-bank.com gets tricked into thinking that a valid request was made. Your account just got compromised without your knowledge!

CSRF tokens are a countermeasure to this type of attack. Essentially, a token is created on the backend and sent to the browser. Then, whenever the browser sends a non-GET request to the backend, the CSRF token is sent in the request’s header, where the backend can verify its authenticity. A malicious website doesn’t have access to this token, so any non-GET request that was triggered while you browsed such a site would not affect your account on www.my-bank.com.

Issue #3: Getting CSRF Tokens to Play Nice with Sessions

The challenge now became to enable CSRF protection with sessions. I tried numerous ways of enabling CSRF protection, but many of the solutions out there either involved the use of Axios for fetching data on the React frontend, or entailed a full-stack Rails application. Below is the only solution that worked for me (kudos to Tomas Valent for his excellent blog post on CSRF tokens in Rails; it saved my sanity!).

I edited my application_controller.rb file to the following:

# application_controller.rbclass ApplicationController < ActionController::Base
include ActionController::Cookies
after_action :set_csrf_cookie
def fallback_index_html
render file: 'public/index.html'
end
private def set_csrf_cookie
cookies["CSRF-TOKEN"] = {
value: form_authenticity_token,
secure: true,
same_site: :strict
domain: 'life-lister.herokuapp.com'
}
end
end

You’ll notice I did create and store the CSRF token inside a cookie, which theoretically means a malicious site could use it just like it could use the session_id to compromise someone. However, when Rails receives a request, it doesn’t read the token from the cookie, but rather reads it from the request header.

I also used some settings when creating the cookie for my CSRF token to enhance security. Setting secure: true prevents my Rails backend from accepting the token if the request was made over the less secure HTTP protocol (instead of HTTPS). Also, I set same_site: :strict, meaning that the token will only be accepted from my domain (which I also defined in the cookie). I could have set httponly: true, which would prevent a malicious site from reading my CSRF token, but doing so would also prevent my frontend from reading it too! (And my frontend needs to read the token to send it in the request headers).

Now, on my frontend, I had to tweak a bit of code as well. I created a new file to handle the logic of reading and grabbing the value from the CSRF token.

# cookies.jsfunction CSRFToken(cookies) {
const splitCookies = cookies.split('; ');
return splitCookies.find(cookie => cookie.startsWith("CSRF-TOKEN=")).split('=')[1];
}
export default CSRFToken;

Then, inside any React component that made a non-GET request, I imported the function and used it inside the request headers:

# Component that needs to send a non-GET requestimport CSRFToken from './cookies';...function handleSubmit(e) {
e.preventDefault();
fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": CSRFToken(document.cookie)
},
body: ...
}
...
}

Here, I pass in document.cookie as an argument to my CSRFToken() function, which grabs all of the cookies stored on my app’s domain. Then, this function parses out the value of the CSRF token and returns it, which is set inside the “X-CSRF-Token” header.

Now, after another redeployment, my app works properly without disabling CSRF protection! :)

Resources

--

--

Megan McCarty
CodeX
Writer for

Passionate web developer with endless curiosity. Current Flatiron Software Engineering student. Portfolio: https://meganmccarty-web-portfolio.netlify.app/