Cookies VS LocalStorage: Dicas de como armazenar dados e tokens (JWT) com segurança

Cookies VS LocalStorage: Dicas de como armazenar dados e tokens (JWT) com segurança

Quando pensamos em construir aplicações para web, é de extrema importância também pensar na segurança das nossas aplicações, principalmente quando estamos trafegando dados sensíveis do usuário.

Um bom exemplo disso são sistemas de login, cadastros, além dos fluxos de recuperação de senha, que necessitam trafegar e armazenar algumas credênciais do usuário dentro de cookies, sessões ou por meio do WebStorage (LocalStorage).

Só que antes de continuarmos você precisa entender (ou recapitular, caso você já saiba) algumas boas práticas 😄

Sempre use Tokens para validar a sessão do usuário

Antes de mais nada, é importante você entender a seguinte premissa:

  • Nunca salve o ID, e-mail e principalmente a senha do seu usuário dentro de cookies, sessões ou webstorage do navegador.

Por se tratar de informações sensíveis, qualquer um que tenha acesso á máquina do seu usuário, poderá recuperar esses dados e ter acesso de mão beijada, o ID do usuário no banco, o e-mail dele, e o pior... a senha dele 🤯

Então se você ainda é aquele tipo de programador, que durante o processo de cadastro/login ainda está salvando o id, email e a senha do seu usuário dentro de cookies/sessões/webstorage, chegou a hora de parar com isso!

Hoje, a forma mais segura que nós temos para autenticar um usuário é por meio de tokens, que podem ser aqueles que você gera pelo seu próprio sistema, ou quem sabe por meio de tokens JWT.

A nova arquitetura de um sistema de login

Vamos supor que você tem uma tela de login simples, onde pede o e-mail e a senha do seu usuário:

A partir do momento que seu usuário informa seu e-mail/senha e tenta acessar a sua aplicação, por de baixo dos panos, uma nova requisição é enviada do front-end para o back-end da sua aplicação.

O back-end por sua vez, recebe os dados da requisição e valida se aquele e-mail e senha estão registrados no banco de dados (geralmente na tabela users), e se estiverem, o back-end vai gerar um token relacionado a sessão daquele usuário, dentro de uma outra tabela chamada de users_session.

Em seguida, o back-end envia de volta ao front-end o token que foi gerado, além de qualquer outra informação relacionada ao sucesso da autenticação (como um success: true, auth: true, message: 'Usuário Logado!' e afins).

Do lado do front-end, ele se responsabiliza em salvar esse token de sessão dentro de um cookie/sessão/webstorage junto com uma data de expiração.

Onde a cada rota (URL) que o seu usuário tentar acessar ou requisições ao back-end enquanto ele estiver logado, novas requisições serão enviadas de volta ao back-end dessa vez informando o Token da sessão.

Fazendo com que o back-end receba esse token, verifique se ele é valido dentro da tabela users_session, e com isso aprove ou não a abertura dessas URLs ou deixar que o usuário execute determinadas ações, e é claro, o back-end sempre retorna um token novo para o front-end (de modo que ele sempre fica se alterando a cada ação do usuário).

Caso der algum problema na hora da autenticação, o back-end retorna essa informação ao front-end, que por sua vez executa os procedimentos necessários para deslogar esse usuário e o levar de volta a tela de login.

Esse é um dos princípios de uma uma arquitetura de login segura. Fora isso, você pode implementar tokens JWT, tokens no header da requisição, autenticação de dois fatores, Google Captcha, OAuth 2.0 e afins.

Dessa forma, você consegue esconder o id, email e a senha do seu usuário.

Observação: Existem sistemas onde a cada requisição de validação de token, um novo token é gerado e enviado ao front-end. Isso faz com que a cada mudança de URL ou uma nova ação por parte do usuário, faça gerar um novo token de sessão, o que adiciona ainda mais segurança a aplicação (essa técnica também é vista em funcionalidades como refresh token ou SALT).

Agora, nós iremos discutir uma das formas mais seguras de se autenticar usuários 🥳

Onde eu deveria armazenar meus tokens em uma aplicação front-end?

Durante a Jornada Javascript, eu cheguei a falar sobre duas formas de se armazenar informações dos nossos usuários no navegador do usuário, são elas:

Apesar de cada um deles possuir suas particularidades, existe muito debate sobre qual é a melhor opção a ser usada, onde a grande maioria dos desenvolvedores optam por armazenar esses tokens dentro de cookies, por acharem mais seguro.

Antes de discurtirmos o assunto principal, vamos dar uma breve analisada nos prós e contrás de cada um deles, começando pelo localStorage.

LocalStorage

Como você já deve saber, o localStorage faz parte da API do Web Storage, servindo como mais um meio de armazenar as informações provenientes dos sites.

Prós: ele é simples de se usar e é bem conviênte, uma vez que utilizamos javascript de forma pura. Além disso, ele pode ser usado com API's que requerem que o você coloque seu token de acesso no cabeçalho, como por exemplo:

Authorization Bearer ${access_token}

Contras: só que, ele é totalmente vulnerável a ataques XSS!

Um ataque XSS, acontece sempre quando um invasor tenta executar algum código Javascript dentro do seu próprio site, e isso pode ser feito por meio de bibliotecas como ReactJS, Jquery, Google Analytics, Vue, Angular e entre outras.

O que permite que o invasor possa obter o token de acesso da sua aplicação, que está armazenado no localStorage (e de forma bem fácil).

Cookies

Como você também já deve saber, um cookie é uma das formas mais antigas que temos para armazenar informações dos sites dentro do navegador dos nossos usuários.

No caso dos cookies, eles possuem um esquema de segurança um pouco melhor se comparado ao localStorage, uma vez que podemos definir restrições em seu acesso, como é o caso do uso dos parâmetros httpOnly e secure.

Prós: quando temos um cookie definido com as regras de httpOnly e secure, temos um cookie que não pode ser acessado via Javascript, ou que o torna menos vulnerável a ataques do tipo XSS.

Isso faz com que o invasor não consiga ler seus tokens de acesso que estão armazenados dentro dos cookies.

Contras: devido às suas limitações de tamanho (apenas 4 KB), se você tiver um token muito grande, talvez não consiga armazená-lo dentro dos cookies.

Além disso, podem existir situações em que sua API exija que você coloque o token de acesso dentro do cabeçalho da requisição (header). E como você vai fazer isso, se você não consegue mais acessar esses cookies via Javascript?

XSS: Cookies VS LocalStorage

Se você estava esperando um vencedor definitivo, infelizmente tenho que te dar a seguinte resposta: DEPENDE DA SUA APLICAÇÃO!

No caso do LocalStorage ele é mais vulnerável pois é facilmente acessível usando Javascript, onde um invasor poderia recuperar seus tokens de modo a usá-los mais tarde.

Entretanto, criar um cookie com as diretrizes de httpOnly e secure, adicionam um certo nível de segurança, mas fazem com que seus tokens não sejam mais acessíveis via Javascript.

Ataques do tipo CSRF

Já um ataque do tipo CSRF, acontece quando um usuário é forçado a fazer uma solicitação de forma não intencional, ou seja, quando existe um script que modifica certas requisições (acontece com muita frequência em formulários).

Por exemplo, observe um exemplo de envio de e-mail abaixo:

POST /email/change HTTP/1.2
Host: micilini.com
Content-Type: application/x-www-form-urlencoded 
Content-Length: 589 
Cookie: token=ey9d9n23d9u2un3dprtrtlv4otmg04r34d34dhhh 

email=micilini@micilini.com
password=1234567890

Um invasor poderia facilmente criar um formulário em um site malicioso com o intuito de enviar uma solicitação via método POST para https://micilini.com/alterar-email, levando um segundo campo de e-mail oculto junto com o cookie da sessão (seria incluso automaticamente na requisição).

Esse tipo de problema poderia ser facilmente resolvido se usássemos o parâmetro sameSite no seu cookie junto a um token anti-CSRF.

Como o localStorage é acessível via Javascript, e o mesmo não possui as mesmas restrições de um cookie (como o sameSite por exemplo), ele ficaria totalmente vulnerável.

Cookies são mais seguros em comparação ao LocalStorage

Como visto anteriormente, embora os cookies ainda apresentem algumas vulnerabilidades, eles são muito mais seguros se comparado ao LocalStorage.

Mas isso não significa dizer que os cookies são vulneráveis a ataques do tipo XSS, porém é mais difícil um invasor realizar este tipo de ataque quando você faz o uso do parâmetro httpOnly.

Além disso os cookies também são vulneráveis a ataques do tipo CSRF, entretanto, podem ser evitados por meio do parâmetro sameSite açém do uso de tokens anti-CSRF.

De acordo com as recomendações do OWASP

Não armazene identificadores de sessão no armazenamento local, pois os dados estão sempre acessíveis por JavaScript. Os cookies podem mitigar esse risco usando o sinalizador httpOnly.

Três maneiras de armazenar seus tokens de forma segura

Abaixo separei três maneiras de se armazenar seus tokens dentro de cookies de forma segura, vejamos cada uma delas:

Opção 1: Você pode armazenar seu token de acesso no LocalStorage (ou use cookies com o parâmetro httpOnly). Tenha em mente que seus tokens podem ser roubados em um ataque XSS.

Opção 2: Você pode armazenar seu token de acesso dentro de cookies com o parâmetro httpOnly informado. É importante ressaltar que ainda assim, essa opção pode estar propensa a ataques CSRF - mas que podem ser mitigados.

Opção 3: Por último você pode armazenar o token de atualização em um cookie usando httpOnly em conjunto com técnicas anti-CSRF. O que garante uma segurança um pouco melhor com relação aos ataques XSS.

Observação: Armazene seu token de acesso (access token) na memória, e seu token de atualização (refresh token) no cookie.

Quando me refiro a "armazene seu token de acesso na memoria", significa que você deve colocá-lo em uma variável (como const accessToken = XYZ) em vez de colocá-lo em localStorage ou dentro de um cookies da vida.

Em termos de envio de um formulário usando um refresh token, ele atuaria como um novo token de acesso que seria retornado, fazendo com que o invasor ficasse impossibilitado de ler a resposta do formulário HTML.

Em contrapartida, para evitar que um invasor faça solicitações de busca ou realize um AJAX de modo a ler a resposta da requisição, você pode aplicar uma política CORS do tipo Authorization Server, filtrando solicitações de sites não autorizados.

Aplicando tokens com segurança no seu projeto

Agora vamos colocar a "mão na massa", e fazer você entender de uma vez por todas, como aplicar todos os conceitos discutidos neste artigo 👏

Supondo que você tenha um sistema de login bem parecido com este abaixo:

E que deseja melhorar ainda mais a segurança de autenticação, siga os passos abaixo 😉

Primeiro Passo) Retorne o token de acesso (access token) e o token de atualização (refresh token) sempre quando o usuário se autenticar.

Quando seu usuário digitar o e-mail, a senha e clicar no botão de enviar. Uma nova requisição do front-end da sua aplicação deverá chegar no back-end. É nesse momento que você irá validar se as credenciais informadas pelo seu usuário (email/senha) são válidas ou não.

Após isso, você deverá retornar ao front-end o seu token de acesso (access token) e o também o token de atualização (refresh token).

Com o token de acesso (access token) em mãos, salve-o no corpo da página. Já o token de atualização (refresh token) salve-o dentro de um cookie usando as seguintes configurações: 

  • Use o parâmetro httpOnly para evitar que o JavaScript o leia.
  • Ative o parâmetro secure, para que o cookie só possa ser enviado via HTTPS.
  • Ative o parâmetro sameSite de modo strict, para evitar ataques do tipo CSRF.

Talvez agora você esteja se perguntando: Como o servidor back-end vai receber um cookie do frond-end (refresh token), se este está setado como httpOnly, ou seja, não pode ser lido pelo Javascript, e consequemente não poderá ser enviado por ele numa requisição?

Quando um cookie é definido como httpOnly, significa que ele não pode ser acessado por scripts do lado do cliente, como JavaScript. No entanto, ele ainda é enviado automaticamente pelo navegador para o servidor em todas as solicitações HTTP, incluindo solicitações AJAX e requisições de formulário.

Portanto, mesmo que o JavaScript no lado do cliente não possa ler diretamente o cookie httpOnly, ele ainda é enviado automaticamente pelo navegador para o servidor em todas as solicitações HTTP, incluindo solicitações AJAX e requisições de formulário. O servidor back-end pode então acessar o cookie httpOnly normalmente, como faria com qualquer outro cookie.

Segundo Passo) Armazene o seu token de acesso (access token) na memória.

Quando me refiro a armazenar o seu token de acesso na memória, estou dizendo para você salva-lo dentro de uma variável no front-end, como por exemplo:

const accessToken = iimfimf0imf03o4rf4r4rf;

E sim, o seu token de acesso (access token) vai desaparecer assim que o usuário fechar o navegado, ou atualizar o site. E é por isso que temos a nossa disposição o token de atualização (refresh token).

Pois não salvando o token de acesso (access token) dentro de um cookie ou um localStorage da vida, fica muito mais dificil para o invasor realizar qualquer tipo de ataque.

Terceiro Passo) Renove o seu token de acesso (access token) usando o seu token de atualização (refresh token).

Agora vem o pulo do gato!

Quando o seu usuário fechar a página, atualizar a página ou quem sabe expirar o token, Isso fará com que o seu token de acesso (access token) se perca (uma vez que ele está sendo salvo na memória, obviamente).

Daí, quando seu usuário entrar novamente na sua página, o front-end deverá acessar novamente algum endpoint da sua aplicação back-end (/refresh-token por exemplo) fazendo com que seu token de atualização (refresh token) que está armazenado em um cookie, seja passado de forma automatica junto com a requisição HTTP (como falamos anteriormente).

De modo que você receba um novo token de acesso (access token) para usa-lo em suas requisições de API.

Caso você estiver usando tokens do tipo JWT, isso também significa que eles podem ser maiores que 4 KB, de modo que você também consiga informa-los no cabelaçho da requisição (header) 😉

Lembre-se que toda vez que seu usuário mudar de página ou fizer alguma solicitação para a sua API, um novo access token deverá ser gerado, onde ele será usado novamente para fazer as validações no back-end da sua aplicação.

Observação: Sempre salve o access token e o refresh token do seu usuário no banco de dados (essa operação deve ser feito no seu back-end, pois dessa forma você identifica se o refresh token e o access token são válidos.)

Conclusão

Se você gostou desse artigo não deixe de comentar, compartilhar e dar aquele like 😆