Executando verificação de segurança...
11

Trabalhando com 'interceptors' do axios para reutilização e renovação de token!

O interceptor possui request e response para ser utilizado antes/depois da chamada ao endpoint.

Declaração básica do Axios.

import axios from 'axios'; 

const api = axios.create({
    baseURL: "Sua Api Aqui" // ex.: http://localhost:3000/
})

Como enviar o bearer token em todas as chamadas sem precisar passar manualmente?

api.interceptors.request.use((request) => {
    // Buscando seu token salvo no localstorage ou qualquer outro local
    const token = localStorage.getItem("token");
    
    if(token) {
        // Authorization geralmente é o header padrão para envio de token, mas isso não é uma regra. O endpoint pode requisitar outro header.
        request.headers.Authorization = `Bearer ${token}`;
    }
    // Este return é necessário para continuar a requisição para o endpoint.
    return request; 
});

Como utilizar a estratégia de Refresh Token e refazer a request após a renovação do token?

api.interceptors.response.use((response) => {
    // Retorna a resposta caso a requisição tenha sucesso.
    return response;
}, async (error) => {
    // O config é responsável por manter todas as informações da sua request.
    const originalRequest = error.config;
    
    // verifica se recebeu status 401 (unauthorized)
    // verifica se já houve mais de uma tentativa de buscar o mesmo endpoint
    if (
      error?.response?.status === 401 &&
      !originalRequest?.__isRetryRequest
    ) {
        originalRequest.retry = true;
        // Buscando seu refreshToken salvo no localstorage ou qualquer outro local
        const refreshToken = localStorage.getItem("refreshToken");
        if(!refreshToken) {
            // Limpa o localStorage para evitar redirecionamento automático da possível configuração em suas rotas
            localStorage.clear();
            // Redireciona automáticamente o usuário para uma rota aqui utilizei "/" que é para login
            return (window.location.href = "/");
        }
        
        // Agora chegou a hora de fazer sua chamada ao endpoint de renovação de token;
        const response = await refresh(refreshToken);
        // Essa parte vária o tipo de formato do retorno do seu endpoint.
        const data = {
            accessToken = response.token;
            refreshToken = response.refreshToken;
        };
        // Transforma o objeto em string e guarda na key "refreshToken";
        localStorage.setItem(JSON.stringify(data), "refreshToken");
        
        // Parte responsável por refazer a request do usuário após a renovação do token
        return api(originalRequest);
    }
    
    // Parte necessária para retornar as requisições que não tiveram sucesso
    return Promise.reject(error);
});
Carregando publicação patrocinada...
6

Excelente post, Gabriel!

Esse é um assunto bem legal, que já me deparei algumas vezes e creio que tenho algo a acrescentar.

Há um fluxo no código não previsto, que já me pegou de surpresa algumas vezes, que é o caso de multiplas requests autenticadas falharem ao mesmo tempo. Nesse fluxo tenho uma sugestão e vou aproveitar para reescrever o código em TypeScript que é o que eu uso =P.

Como é um fluxo extenso, vou tentar deixar tudo bem comentado, para ajudar a explicar a motivação de cada decisão.

Criação do axios básica

import axios, { AxiosError } from 'axios'; 

const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL //Url base da API
})

Enviar o token em todas requests

  api.interceptors.request.use(request => {
    //Por algum motivo os headers podem vir undefined
    //Para não ter problemas adiciono um valor default
    const headers = request.headers ?? {} 

    //Uma sugestão é o uso de cookies ao invés de localstorage
    //especialmente em NextJS, pois nele você consegue acessar
    //os cookies do lado do cliente e no serverside

    //Uma sugestão para get/set de cookies é esse artigo:
    //https://www.w3schools.com/js/js_cookies.asp
    const token = Cookies.get("token")

    if (token) {
      headers['Authorization'] = `Bearer ${token}`
    }

    request.headers = headers
    return request
  })

Definindo uma estratégia de refresh token

//Cria um tipo que irá servir de base para as requests falhadas
type FailedRequestQueue = {
  onSuccess: (newToken: string) => void
  onFailure: () => void
}

//Cria uma array para salvar essas requests
let failedRequestsQueue: FailedRequestQueue[] = []
//Cria uma variável que irá determinar se já está
//acontecendo um processo de refresh, para não gerar duplicidade
let isRefreshing = false

api.interceptors.response.use(response => {
  return response //Em caso de sucesso só retorna a resposta
}, (error: AxiosError) => {
  //Detecta se foi um erro de autorização
  if (error.response?.status === 401) {
    const refreshToken = Cookies.get("refreshToken")
    //Uma sugestão aqui é verificar o tipo do erro retornando
    //pois um token pode ser inválido, por exemplo, e nesse caso
    //gerar um novo token usando o refreshtoken pode ser um
    //problema de segurança. Logo o token tem que ser válido
    //porém está expirado.
    //E para fechar com chave de ouro esse ponto, 
    //ao fazer o refresh token o backend deverá invalidar o token.

    //Somado a isso verificar se há um refresh token salvo
    if (error.response.data?.code === "token.expired" && refreshToken) {
      //Busca as informações da request. Com elas é possível
      //repetir a request passando as mesmas informações
      const originalRequest = error.config

      //Verifica se já está acontecendo um processo
      //de refresh
      if (!isRefreshing) {
        //Seta como true para acontecer só 1 refresh por vez
        isRefreshing = true

        //Chama a API de refresh token
        refreshTokenAPI(refreshToken)
          .then(({data}) => {
            //Em caso de sucesso atualiza os tokens nos cookies
            Cookies.set("token", data.token)
            Cookies.set("refreshToken", data.refreshToken)

            //Retenta todas requests falhadas com o novo token
            failedRequestsQueue.forEach(request => {
              request.onSuccess(data.token)
            })
          }).catch(_ => {
            //Em caso de falha, podemos tomar diferentes
            //estratégias dependendo do contexto.
            
            //Tenho duas sugestões para esse cenário:

            //A. O famoso "Sai do fusca, entra no fusca".
            //Limpo os tokens e mando o usuário para o login
            Cookies.delete("token")
            Cookies.delete("refreshToken")
            window.location.href = "/login"

            //B. Retorno o erro nas requests e cada uma faz um
            //tratamento próprio, como mostra um pop-up, enviar para
            //uma página de erro etc.
            failedRequestsQueue.forEach(request => {
              request.onFailure()
            })
          }).finally(() => {
            //Limpo a fila de requests falhadas
            failedRequestsQueue = []
            //Informo que o processo de refreshing já terminou, para caso
            //ocorra outro erro de token expirado na mesma sessão
            isRefreshing = false
          })
      }

      //Retorna uma promise que vai aguardar o processo de
      //refresh token ser realizado.
      return new Promise((resolve, reject) => {
        //Adiciona na lista de requests falhadas a request atual
        failedRequestsQueue.push({
          onSuccess: (newToken: string) => {
            //Em caso de sucesso será recebido o novo token
            //Basta substituir ele na request e tentar novamente
            originalRequest.headers!['Authorization'] = `Bearer ${newToken}`

            resolve(api(originalRequest))
          },
          onFailure: () => {
            //Em caso de erro, repassa o erro original
            //para que seja feito um tratamento específico
            //de acordo com o contexto da chamada.
            reject(error)
          }
        })
      })
    } else {
      //Em caso de erro 401 que não seja token expirado
      //ou não haja refresh token.
      //Limpa os tokens e manda para a rota de login
      Cookies.delete("token")
      Cookies.delete("refreshToken")
      window.location.href = "/login"
    }
  }
  //Caso o tipo do erro seja outro, retorna ele normalmente
  return Promise.reject(error)
})
1
1

Que mão na roda para quem está procurando uma solução principalmente para refresh de tokens 😍 Muito obrigado por vir postar essa dica aqui. Isso me inspirou a postar pequenas dicas vindo do TabNews também 🤝

Em paralelo, sugiro abrir os blocos de código definindo a linguagem, por exemplo:

```js

// código JavaScript

alert('mensagem');

```

Isso irá fazer com que seja ativado o syntax highlight 👍

// código JavaScript
alert('mensagem');
2
1
1
1