How we solved the “can't verify CSRF token authenticity” problem

jun. 09, 2023

How it started

Some time ago, one of the Platanus developers asked for help in the Slack channel where we have our technical conversations. The message went like this:

Strange indeed. Digging deeper, the message that came along with the exception was a bit more informative:

Can't verify CSRF token authenticity

We started a thread to discuss the problem and possible solutions. It ended up being 37 messages long, and we realized that everyone had a different way of solving the problem. After a constructive discussion, we agreed on the best solution. So I'm going to share what we learned here to help others who may be struggling with the same issue.

The problem

I could explain Cross-Site Request Forgery (CSRF) attacks and the countermeasures against them. But the explanation found in the official Rails guide for Securing Rails Applications is great. If you don’t know anything about this topic, go ahead and check it out.

What you need to know is that one of the counter-measures against CSRF attacks is the use of security tokens. Luckily for us, Rails apps have this protection enabled by default. A security token is included in all Ajax requests generated by Rails. Each time the server receives a request, it verifies the token. If the security token doesn't match the expected one, an exception is thrown.

But what if the request isn’t generated by Rails? In some cases, a Rails app may have a JavaScript framework integrated into its views. Requests coming from this JavaScript client don’t include the CSRF token. So when the server tries to perform the verification, there’s no token to verify and an exception is thrown. This is exactly what had happened to my colleague.

And why was the GET request working well? It’s because a GET request is like a query, a read operation, or a lookup, which means it’s a safe operation. Then, CSRF token verification is not enforced on this type of request.

The solutions

Both the Slack thread and a Google search turned up many solutions. But many of them relied on bypassing the CSRF attack protection mechanism. Here are some of the more common ones I found:

  1. Changing Rails’ default forgery protection strategy
class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end 

Adding this line to your application controller changes the default token verification strategy. Instead of throwing an exception, it nullifies the session during the request, allowing the fraudulent request to go through.

2. Deactivating Rails forgery protection for certain types of requests

class ApplicationController < ActionController::Base
  protect_from_forgery unless: -> { request.format.json? }
end 

This option skips forgery protection, but only for certain types of requests. It assumes that the JavaScript client is handling the session for you instead of Rails, but that may not be true. As in our case.

3. Skipping the token verification altogether

class ApplicationController < ActionController::Base
  skip_before_action :verify_authenticity_token
end

This is the simplest solution. It skips the token verification for all requests. No token verification is performed, ever.

Disabling a security feature like CSRF token verification is risky. So why do some solutions suggest doing it? Well, it could be due to ignorance. Sometimes APIs are used for server-to-server communication with trusted partners. In those cases, you can give them a unique key-secret pair to securely authenticate them. But for modern web apps, where individuals make requests from a JavaScript client, you don't want to use a single key-secret pair for everyone. Instead, you use a session or token-based method to identify each user. Unless you have another way to do this, it's best to stick with the built-in Rails method.

OK, so how do you use this token-based authentication when making POST requests from a Javascript client? The answer is found in the Rails docs:

When using another library to make Ajax calls, it is necessary to add the security token as a default header for Ajax calls in your library. To get the token, have a look at <meta name='csrf-token' content='THE-TOKEN'> tag printed by <%= csrf_meta_tags %>in your application view.

Armed with this new knowledge, we were ready to tackle the problem without disabling important security measures.

How it ended

Going back to our main problem, the best way to solve it was to add the CSRF token to all requests generated by the Javascript client.

At Platanus, we build applications using Rails and integrate Vue.js into their views. And we use a library called Axios to handle client requests. In the configuration of this handler, we already had some headers set, so it was easy to add another one with the CSRF token.

First, we need to get the token. We can do this following the advice of the Rails docs:

// utils/csrf.js
function csrfToken() {
  const meta = document.querySelector('meta[name=csrf-token]');
  const token = meta && meta.getAttribute('content');

  return token ?? false;
}

export { csrfToken };

Then we add the CSRF token header to the request handler configuration:

// index.js
import axios from 'axios';
import { csrfToken } from 'utils/csrf';

const api = axios.create({
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-CSRF-Token': csrfToken(),
  },
});

export { api };

Finally, we use our updated middleware on our API clients. For example, if we have a subscriptions API controller:

// subscription.js
import api from 'index';

export default {
  create(subscription) {
    const path = '/api/internal/subscriptions';

    return api({
      method: 'post',
      url: path,
      data: {
        subscription,
      },
    });
  },
};

And that’s it. The ActionController::InvalidAuthenticityToken exception is gone.

If you are thinking about building a Rails app, you might want to consider using Potassium. It's a web app generator that we made at Platanus and it comes with a bunch of features that help make development easier. Including the one we talked about in this article.

¡Genial! Te has suscrito con éxito.
¡Genial! Ahora, completa el checkout para tener acceso completo.
¡Bienvenido de nuevo! Has iniciado sesión con éxito.
Éxito! Su cuenta está totalmente activada, ahora tienes acceso a todo el contenido.