Olá amigos, tudo bem com vocês?

Vocês por acaso já passaram por um problema do tipo?

Imagem de erro

Started POST "/users" for 127.0.0.1 at 2021-10-01 16\:35\:32 -0300
Processing by UsersController#create as JSON
  Parameters: {"user"=>{"name"=>"Tech", "last_name"=>"Lover"}}
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 2ms (ActiveRecord: 0.0ms | Allocations: 897)

Isso acontece porque em requisições do tipo POST, o nosso queridíssimo Rails faz uma validação do CSRF Token e caso esse mesmo token não seja válido, esse erro acontece.

O motivo do Rails realizar essa validação em requisições POST é porque esse método é usado (normalmente) para criação/edição de recurso nas aplicação, então precisamos de uma segurança maior.

Por exemplo, usamos uma requisição POST para cadastro de um novo usuário ou para alteração de senha.

Se você está pensando como o Rails faz quando utilizamos os form helpers (form_with, form_for, ...) para criação de formulários fico feliz e vou lhe dizer o que ele faz. Ao utilizar esses forms helpers, automaticamente o Rails adiciona um campo escondido com o nome authenticity_token e preenche com o valor correto, então quando você envia os dados formulário para o backend, o token vai junto.


<form autocomplete="none" id="form-form" action="/users" accept-charset="UTF-8" method="post">
    <h4 class="card-title" id="basicModalLabel1">Novo Usuário</h4>
    <input type="hidden" name="authenticity_token" value="ndkQ/YDUyOabT5EVIcfWhpBUJA78VnZOznw36QwS7El/CrsXCeC1ohDim1k/plzkPPHWVieqO+8f+iOGFqcTWA==" />

    <label>Nome<label>
    <input maxlength="200" placeholder="Nome" type="text" name="user[name]" />
    
    <label>Sobrenome<label>
    <input maxlength="200" placeholder="Sobrenome" type="text" name="user[last_name]" />

    <button type="submit">Cadastrar</button>
</form>

O código acima foi gerado utilizando um form helper do Rails (você pode ler mais sobre eles aqui). Perceba que logo no início do formulário foi adicionado um input do tipo hidden preenchido com o valor do token. Esse valor é enviado ao backend quando clicado no botão de enviar o formulário.

Quando a requisição chega no backend, o Rails valida esse token antes de executar qualquer ação dos controllers. Caso validado o token, daí o controller daquela rota é chamado. Se o token não for válido (exemplo: caso seja um bot tentando criar vários usuários automaticamente), o Rails bloqueia todo o restante. Todo isso é feito de graça pelo Rails, bom né?

Obs: Tal validação não existe caso você tenha criado sua aplicação no modo AP

rails new my_api --api

Obs 2: Você pode entender um pouco mais sobre form helpers vendo esse artigo do medium: Rails 5.1's form_with vs. form_tag vs. form_for

Mas digamos agora que você deseja fazer uma requisição POST assíncrona, utilizando JQuery ou Fetch API, como podemos enviar esse token para o backend, podemos solucionar esse problema de duas formas

Forma Errada

Eu vou demonstrar a forma errada, somente por questões educacionais e para mostrar um pouco mais como o Rails funciona.

A forma errada de resolver esse problema é bem simples, basta adicionar 1 linha de código em ApplicationController

# application_controller.rb

class ApplicationController > ActionController::Base
  skip_before_action :verify_authenticity_token
end

Com apenas essa linha de código, estamos dizendo para o Rails não verificar o CSRF Token e como todos nossos controllers herdam do ApplicationController, essa alteração vai fazer efeito para toda nossa aplicação.

O que isso vai acarretar é que qualquer requisição POST, que atenda o padrão definido nas rotas e os parametros definidos no controller, serão aceitas pelo Rails.

Isso torna a aplicação menos segura e você só deve fazer isso caso esteja desenvolvendo uma API que deverá ser usada por diversas outras pessoas. Nesse caso você deve criar uma nova forma de validações, como por exemplo usar o JWT (irei escrever sobre ele em breve)

Forma Correta

Para entender a forma correta, precisamos saber uma coisa. Por padrão, o application layout vem com as seguintes tags no head:

<!DOCTYPE html>
<html>
  <head>
    <title>RailsDevTest</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

E para a gente, importa conhecermos a tag csrf_meta_tags, ela é responsável por adicionar o CSRF Token no head da página toda vez que a página é carregada e é de lá que vamos obter o token para fazer nossas requisições POST.</p>


<!DOCTYPE html>
<html>
    <head>
        <title>Rock Who Codes</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <meta name="csrf-param" content="authenticity_token" />
        <meta name="csrf-token" content="6RVjKaOIHa9S3W3dWQr6MNLMHrNs+FvudwQMIUk4O+V5XJrN2h9Ct+hRhlmMzZ/qMPw/1dH6srHEQelD4ivR4w==" />
    </head>
    <body>
        ...
    </body>
</html>

Podemos obter esse token utilizando VanillaJS (javascript puro) ou JQuery, vou demonstrar as duas formas:

  • VanilaJS
const token = document
  .querySelector("meta[name='csrf-token']")
  .getAttribute("content");
  • Jquery
const token = $("meta[name='csrf-token']").attr("content");

De posse do token, agora basta realizarmos nossa requisição POST, enviando-o para o backend, mas atenção, você deve chamar o parametro de authenticity_token é assim que o Rails vai buscar para validar.

  • VanillaJS
fetch("/path/to/resquest", {
  headers: {
    "content-type": "application/json",
    accept: "application/json",
  },
  body: {
    authenticity_token: token,
    user: {
        name: "Tech",
        last_name: "Lover",
    }
  },
})
  .then((res) => res.json)
  .then((res) => console.log(res));
  • Jquery
$.ajax({
  url: "/path/to/request",
  method: "post",
  dataType: "json",
  data: {
    authenticity_token: token,
    user: {
        name: "Tech",
        last_name: "Lover",
    }
  },
}).done((res) => {
  console.log(res);
});

Obs 3: Muito cuidado. O nome deve ser exatamente igual a authenticity_token e ele deve ir na raiz do objeto JSON.

Agora você já consegue enviar requisições POST sem problemas!

Até a próxima!

#Fim