Executando verificação de segurança...
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)
})
Carregando publicação patrocinada...
1