Hoje eu vou te mostrar como eu resolvo o problema de refresh token nas minhas aplicações! 🤩
O problema:
A um tempo atrás eu estava com problemas de refreshToken na aplicação de um cliente, o que acontecia era que a página Dashboard chamava 3 endpoints diferentes, e quando dava erro 401 (Unauthorized), meu interceptor que criei dentro do Axios disparava 3 vezes a requisição de refreshToken ocasionando uma série de erros.
Então bora fazer isso funcionar?!
Primeiro passo, o que é Axios?
O Axios é um cliente HTTP, que funciona tanto no browser quanto em Node.Js. A biblioteca é basicamente uma API que sabe interagir tanto com XMLHttpRequest quanto com a interface HTTP do Node. Isso significa que o mesmo código utilizado para fazer requisições Ajax no browser também funciona no lado do servidor.
E agora que você já sabe o que é Axios, vamos falar sobre Interceptors
Os Interceptors são como o próprio nome diz, interceptadores. No Axios a gente consegue utiliza-los para executar alguma função antes que a Request e/ou Response seja iniciada.
Segue um exemplo:
// Adicionando um interceptor de Request
axios.interceptors.request.use(function (config) {
// Faça algo antes que a solicitação seja enviada
return config;
}, function (error) {
// Faça algo com erro da solicitação
return Promise.reject(error);
});
// Adicionando um interceptor de Response
axios.interceptors.response.use(function (response) {
// Qualquer código de status que esteja dentro de 2xx fará com que essa função seja acionada
// Faça algo com os dados de resposta
return response;
}, function (error) {
// Quaisquer códigos de status que estejam fora de 2xx fazem com que esta função seja acionada
// Faça algo com erro de resposta
return Promise.reject(error);
});
Esse exemplo você pode encontrar na própria documentação do Axios aravés desse link.
Pronto, agora chega de enrolação, e vamos para o que interessa!
⚠️ Alerta!
Um ponto importante, é que nesse post eu vou pular as etapas de criar uma aplicação React e fazer a instalação do Axios, então caso você não saiba como fazer algum desses passos só olhar a documentação do React ou do Axios.
Continuando…
1º Primeiro passo:
Vamos criar um arquivo chamado api.ts que vai ser responsável por armazenar toda a configuração do Axios que vai ficar dessa forma:
import axios from "axios";
export function api() {
const api = axios.create({
baseURL: "http://localhost:3333",
headers: {
Authorization: Bearer your_token,
},
});
return api;
}
Nesse arquivo, criamos a configuração inicial para usar o Axios na aplicação.
2º Segundo passo:
Agora vamos criar o nosso interceptor que vai ser responsável por validar se houve um erro de status 401 para realizarmos o refreshToken.
import axios, { AxiosError } from "axios";
import { signOut } from "hooks/Auth";
// Variavel para informar se está acontecendo uma requisição de refresh token
let isRefreshing = false;
// Variavel para armazenar a fila de requisições que falharam por token expirado
let failedRequestQueue = [];
// Tipagem dos dados de response da api de autenticação
type AuthApiResponse = {
token: string;
refreshToken: string;
};
export function api() {
// Cria as configurações iniciais do Axios
const api = axios.create({
baseURL: "http://localhost:3333",
headers: {
Authorization: Bearer your_token,
},
});
// Cria um interceptor para interceptar todas as requisições que forem feitas
api.interceptors.response.use(
(response) => {
// Se a requisição der sucesso, retorna a resposta
return response;
},
(error: AxiosError) => {
// Se a requisição der erro, verifica se o erro é de autenticação
if (error.response.status === 401) {
// Se o erro for de autenticação, verifica se o erro foi de token expirado
if (error.response.data?.code === "token.expired") {
// Recupera o refresh token do localStorage
const refreshToken = localStorage.getItem("refreshToken");
// Recupera toda a requisição que estava sendo feita e deu erro para ser refeita após o refresh token
const originalConfig = error.config;
// Verifica se já existe uma request de refreshToken acontecendo
if (!isRefreshing) {
// Se não existir, inicia a requisição de refreshToken
isRefreshing = true;
// Faz uma requisição de refreshToken
api
.post("/refresh", {
refreshToken,
})
.then((response) => {
// Recupera os dados do response e cria o newRefreshToken por que já está sendo utilizado a variável refreshToken
const { token, refreshToken: newRefreshToken } =
response.data as AuthApiResponse;
// Salva o token no localStorage
localStorage.setItem("token", token);
// Salva o refreshToken no localStorage
localStorage.setItem("refreshToken", newRefreshToken);
// Define novamente o header de autorização nas requisições
api.defaults.headers["Authorization"] = Bearer your_token;
// Faz todas as requisições que estavam na fila e falharam
failedRequestQueue.forEach((request) =>
request.onSuccess(token)
);
// Limpa a fila de requisições que falharam
failedRequestQueue = [];
})
.catch((err) => {
// Retorna os erros que estão salvos na fila de requisições que falharam
failedRequestQueue.forEach((request) => request.onFailure(err));
// Limpa a fila de requisições que falharam
failedRequestQueue = [];
// Caso der erro desloga o usuário
signOut();
})
.finally(() => {
// Indica que a requisição de refreshToken acabou
isRefreshing = false;
});
}
// Usando a Promise no lugar do async await, para que a requisição seja feita após o refresh token
return new Promise((resolve, reject) => {
// Adiciona a requisição na fila de requisições que falharam com as informações necessárias para refazer a requisição novamente
failedRequestQueue.push({
// Se a requisição der sucesso, chama o onSuccess
onSuccess: (token: string) => {
// Adiciona o novo token gerado no refresh token no header de autorização
originalConfig.headers["Authorization"] = Bearer your_token;
// Faz a requisição novamente passando as informações originais da requisição que falhou
resolve(api(originalConfig));
},
// Se a requisição der erro, chama o onFailure
onFailure: (err: AxiosError) => {
// Se não for possivel refazer a requisição, retorna o erro
reject(err);
},
});
});
} else {
// Caso der erro desloga o usuário
signOut();
}
}
// Se não cair em nenhum if retorna um error padrão
return Promise.reject(error);
}
);
return api;
}
Todo o código com o interceptor e a nossa fila de requisição fica assim, mas calma que eu vou te explicar o que está rolando!
- Linha 11: Criamos uma variável para saber se já está acontecendo um refreshToken em caso de multiplas requisições que retornaram erro
- Linha 13: Criamos uma variável para armazenar todas as requisições que deram erro, criando a nossa fila de requisições
- Linha 31: Estamos criando o nosso Interceptor
- Linha 32: Se a requisição der sucesso, apenas retornamos o response original
- Linha 36: Todo erro que a requisição retornar cai nessa parte
- Linha 38 – 40: Nessas linhas estamos apenas verificando se o erro que foi retornado pela API é um erro de autenticação
- Linha 44: Recuperamos todo a request original para refazer quando o refreshToken for concluído
- Linha 47: Verifica se não está acontecendo um fluxo de RefreshToken
- Linha 51: Inicia a chamada para a API de refreshToken
- Linha 70: Depois que a requisição de refreshToken for concluída, vamos realizar todas as outras requests que estavam na fila com o novo token gerado
- Linha 74: Estamos voltando a fila para seu estado original
- Linha 92: Quando já está acontecendo um refreshToken, nosso código vai cair nessa linha e vai recuperar e armazenar todas as requisições que falharam e vai armazenar na nossa variável de fila para ser executado após o sucesso do refreshToken
- Linha 112: Caso der erro, deslogamos o usuário
Esse foi um breve resumo do que cada linha está fazendo, mas todo o código está comentado, para facilitar seu entendimento!
Pronto!
Agora toda vez que você tiver um ou mais erros de 401 (Unauthorized) na sua aplicação, automaticamente vai ser feito um refreshToken e vai ser chamado novamente as requisições que falharam sem precisar atualizar a tela!
Simples né?!
Bom, é isso, espero que tenha gostado!