Corrigindo o problema no Rails de InvalidAuthenticityToken em requisições assíncronas
Olá amigos, tudo bem com vocês?
Vocês por acaso já passaram por um problema do tipo?
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
Opa, tudo bem? Antes de você ir, te deixo aqui meu email: [email protected]
Teve alguma dúvida?
Você achou que eu escrevi alguma coisa errada? Erro de português?
Você tem uma forma melhor de resolver o problema?
Você tem alguma sugestão de tópico para eu escrever?
Ou se quiser conversar sobre qualquer outro assunto?
Vamos trocar uma ideia. Sinta-se livre para me enviar um email. Eu lhe responderei com o maior prazer!