Autenticação com JWT (JSON Web Token) no React

Arley Souto
13 min readJan 7, 2024

--

Muitas aplicações utilizam o JWT para autenticar os seus usuários, basicamente é um token que carrega pedaços de dados suficientes para facilitar o processo de determinar a identidade de um usuário. De modo geral, o token é um artefato que permite que a aplicação execute o processo de autorização e autenticação.

Na maioria das aplicações hoje em dia é utilizado uma API de autenticação, isso facilita muito no desenvolvimento de qualquer projeto por não ter que se preocupar com essa parte de autenticação do usuário, afinal de contas a API autenticação garante a segurança e integridade dos dados do usuário, existem várias que podemos utilizar, por exemplo OAuth do Google, Linkedin, Github, entre outras.

Então se você é um programador, em algum momento da sua vida profissional você já se deparou com o JWT, mas caso ainda não tenha se deparado com ele ainda, sua hora vai chegar. Resolvi escrever esse artigo porque quando eu tive a felicidade de trabalhar com JWT tive alguns problemas na hora de trabalhar com o refresh token, na época pesquisando encontrei varias formas de se fazer, funciona, mas eu queria uma forma mais organizada e melhor então resolvi vir aqui para mostrar passo a passo. Isso depois de uma aula da Rockeseat.

A tecnologia que vou utilizar aqui é o React Native mas pode ser aplicado em outras tecnologias também, só adaptar. No projeto estou utilizando o useContext no gerenciamento global de estado, o axios para requisições, além de usar a biblioteca AsyncStorage para armazenar o token, vamos precisar. Vamos lá!

A estrutura de pastas está da seguinte forma:

A primeira coisa que vamos fazer é configura o AsyncStorage, onde vamos armazenar algumas informações de autenticação. Para isso foi criada uma pasta storage onde vamos criar os arquivos para fazer esse gerenciamento. Vamos criar um arquivo chamado storageConfig.ts onde vamos deixar as nossas keys do armazenamento local.

No arquivo vamos criar as keys que servirão de referencia para armazenar os dados, essa parte é bem simples:

A próxima parte é criar um arquivo onde vamos fazer todo o gerenciamento dos tokens, esse aquivo vai salvar, buscar e deletar os tokens armazenados localmente.

No arquivo de storageAuthToken.ts digite o seguinte código, na linha 3 estamos importando a chave que criamos no storageConfig.ts, na linha 5 estamos tipando os argumentos da linha 10, onde tem uma função que vai salvar os tokens, na linha 14 temos uma função que vai buscar os dados local, e por fim na linha 22 vai apagar os tokens quando o usuário deslogar.

Agora vamos para o contexto, eu não vou entrar muito afundo como configura o contexto da aplicação, mas não tem segredo.

No contexto a tipagem que vamos utilizar é bem simples. Nas tipagens eu não vou entrar tão afundo.

No contexto foram criados apenas dois estados, o user e o isLoandingUserStorage.

Quando o usuário loga, é feito uma requisição para a API, a resposta da requisição são os dados do usuário, e junto com eles vem o os tokens, precisamos guardar esses tokens localmente, e para isso vamos utilizar as funções que criamos anteriormente para o armazenamento local. Além de armazenar o token é necessário passar o token para cabeçalho da requisição.

Na linha 51 é feito uma verificação para ver se os dados de usuário, o token e refresh token existem, se os dados existir vai entrar na condição, e com isso vou repassar essas informações para a função na linha 53 e na linha 55 também.

A função da linha 55, vai receber o token e o refresh token no header, além disso os dados de usuário serão adicionado no estado.

Na linha 53 é a função que vai fazer o armazenamento dos tokens e dados do usuário localmente. Podemos ver isso na linha 36 e 37, mas o que nos interessa são os tokens.

Agora que temos os token e o refresh token armazenados localmente, vamos para a parte da configuração do nosso arquivo de API. No arquivo de API vamos configurar nosso interceptors, que vai monitorar a validade do token.

Refresh token

O refresh token é uma estrategia de buscar por um token atualizado de uma forma automática, o token ele tem um período de validade, isso é definido no backend. Quando o usuário envia uma requisição para o backend junto a requisição vai o token de autorização. No momento que o usuário faz a autenticação na aplicação, inserimos o token no cabeçalho na parte de autorização de todas as requisições que o usuário vai fazer dali para frente, junto com a requisição o token vai anexado.

No backend vai ser validado a requisição, no qual vai ser feito a verificação se o token é valido. Já na aplicação será feito a interceptação da resposta do backend para verificar se na resposta tem erro, se na requisição não houver erro o fluxo vai seguir normalmente, caso contrario se houver será feito uma verificação se o erro é especifico do token invalido ou se é um outro erro qualquer. Se for qualquer outro erro, então será interceptado e a promise será rejeitada e retornada para o backend fazer o tratamento dessa exceção. Se for um erro específico com o token, será interceptado e não vai ser rejeitado, a requisição vai ser armazenado em uma fila de espera, para que o usuário não perca o seu trabalho.

Exemplo: O usuário está atualizando o perfil dele, ele preencheu vários campos, quando ele faz a requisição, por algum motivo o token dele está expirado, ele vai perder tudo o que ele fez.

Então para garantir que o usuário não vai perder essa requisição, a requisição será adicionada em uma fila de espera. Quando a requisição for adicionada a fila de espera, o token atual que é o invalido que vai ser recuperado do dispositivo do usuário, com esse token antigo vai ser feito uma requisição para a rota do backend que vai atualizar o token, então esse novo token será adicionado ao cabeçalho, que é o headers.

Agora no arquivo api.ts será feito as configuração para interceptar os tokens, mão na massa.

No arquivo vamos criar uma tipagem que vai conter uma função personalizada, que vai gerenciar a interceptação do token na aplicação. Depois de criar a tipagem basta colocar na api, essa parte está feita.

Agora vamos voltar lá no contexto porque precisamos ter acesso ao signOut no arquivo de api. Em nosso contexto vamos criar um useEffect, onde nesse hook vamos criar uma constante chamada subscribe, que vai receber a função registerIterceptTokenManager recebendo no método o signOut, com isso quando a aplicação carregar esse useEffect será executado. Quando é feito essas funções de registro no useEffect, uma regra de boas práticas é limpar a memoria.

Agora em nosso arquivo de api vamos em nossa função de register e criar uma constante. Vamos pegar a linha 20 e colocar nessa constante.

Vai ficar assim, bem simples essa parte.

Então, criamos uma constante e essa constante está recebendo o interceptor, com isso podemos usar um outro método dos interceptadores do axios, que é dar um eject nesse interceptor depois de usar ele.

Logo abaixo está o código completo até agora, na linha 16 onde está escrito erro, vamos mudar para requestError para saber que esse erro é o original, porque podemos ter outros erros, assim poderemos saber que esse é o erro da requisição.

Ficará assim

O que foi feito até agora foi o gerenciador que vai interceptar o token para verificar se é um token válido, para então fazer a requisição de um novo token caso seja invalido. Agora é preciso fazer uma verificação para ver se o token é invalido ou se é um token expirado. E para isso é preciso saber se no resquestError tem um response e se dentro do response tem um status 401, se houver um status 401 então significa que temos uma requisição não autorizada, isso é um indício que é um erro relacionado ao token.

Agora vamos fazer mais uma validação, para verificar se no response do resquetError existe um data, onde vai ter uma mensagem de token expirado ou invalido.

Se for um erro 401 e não está relacionado ao token, então o usuário será deslogado, para que o usuário comece o fluxo novamente.

Agora vamos recuperar o refresh token do nosso armazenamento local, e para ter certeza que temos um refresh token armazenado, vamos fazer uma verificação. Se o refresh token não existir, o usuário será deslogado e daremos um return em nossa promise.

Agora vamos colocar as requisições na fila de espera, para que possamos processar essas requisições novamente com o token atualizado. Para isso vamos criar uma variável onde vai ser armazenado essa fila, em seguida vamos cria uma tipagem, onde teremos dois métodos, um de sucesso e outro de falha.

Com a tipagem PromiseType criada vamos tipar a nossa variável failedQueue.

Agora que temos a nossa fila, podemos adicionar as requisições nela, então se o usuário realmente tem um refresh token, ele não entra na verificação de refresh token vazio e é deslogado. Então a primeira coisa que iremos fazer é pegar a requisição original, na requisição original tem todas as configurações da requisição que foi feita.

Vamos criar agora uma variável auxiliar, que vai ser um booleano, sempre que entrar no fluxo vai ser feito a verificação se está sendo feito a solicitação de um novo token, a variável inicia como falso, então a primeira vez que a requisição passar pela verificação não vai entrar na condição, porque o valor é falso, e se ele passar uma segunda vez o valor vai estar verdadeiro.

Na condição, onde vai ser feito a logica de adicionar as requisições na fila.

Agora dentro da condição, vamos pegar a nossa fila e adicionar as requisições e adicionar os métodos de onSuccess e onFailure.

No método de sucesso vamos pegar o token para processar a requisição como um novo token. Por fim devolvemos para a api processar a requisição.

E se der errado, vai ser rejeitado a requisição. Dessa forma temos a fila de requisições implementada.

Ainda não acabou, estamos no final. Agora vamos buscar o token atualizado, para isso precisaremos criar uma nova promise.

Agora vamos buscar pelo token atualizado, agora de qual lugar vamos buscar esse token atualizado? Isso varia de api para api. Como eu descubro isso? Na documentação, com seu gerente, com a equipe. No meu caso eu tenho uma rota onde ele me traz esse token atualizado. Depois de obter o token, precisamos armazenar ele. Se falhar, pegamos a fila de requisição e percorremos cada requisição que está lá dentro, e usar o método para dizer que falhou passando o erro e deslogando o usuário e rejeitando a requisição. E por fim no finally iremos alterar o valor para falso, que significa que o nosso token já está atualizado, e limpar a fila.

Agora que temos o token atualizado, já estamos armazenando ele no dispositovo ou no navegador, precisamos finalizar o fluxo, que é reenviar a requisição e processar as requisições novamente. Para isso vai ser preciso fazer uma condição para verificar se dentro da requisição original existe um data lá dentro, se existir é preciso atualizar o cabeçalho, tanto das próximas requisições quanto dessa requisição que passou.

Então vamos começar pela requisição que passou por aqui, passando o token pelo cabeçalho, e atualizando a api. Precisamos percorrer a fila de requisições, processando e passando o onSuccess para processar as requisições que estão na fila.

E por fim passando o resolve, para enviar e processar essa requisição. E com isso finalizamos.

Espero que eu tenha ajudado, estarei deixando o código completo por aqui.

API

import { storageAuthTokenGet, storageAuthTokenSave } from "@storage/storageAuthToken";
import { AppError } from "@utils/AppError";
import axios, { AxiosError, AxiosInstance } from "axios";

type SignOut = () => void

type PromiseType = {
onSuccess: (token: string) => void;
onFailure: (error: AxiosError) => void;
}

type APIInstanceProps = AxiosInstance & {
registerInterceptTokenManager: (signOut: SignOut) => () => void
}

const api = axios.create({
baseURL: 'http://192.168.0.11:3333'
}) as APIInstanceProps;

let failedQueue: Array<PromiseType> = [];
let isRefreshing = false;

api.registerInterceptTokenManager = signOut => {
const interceptTokenManager = api.interceptors.response.use(response => response, async (requestError) => {
if (requestError?.response?.status === 401) {
if (requestError.response.data?.message === 'token.expired' || requestError.response.data?.message === 'token.invalid') {
const { refresh_token } = await storageAuthTokenGet()

if (!refresh_token) {
signOut();
return Promise.reject(requestError);
}

const originalRequestConfig = requestError.config;

if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
onSuccess: (token: string) => {
originalRequestConfig.headers = { 'Authorization': `Bearer ${token}` };
resolve(api(originalRequestConfig))
},
onFailure: (error: AxiosError) => {
reject(error);
},
})
})
}

isRefreshing = true;

return new Promise(async (resolve, reject) => {
try {
const { data } = await api.post('/sessions/refresh-token', { refresh_token });
await storageAuthTokenSave({ token: data.token, refresh_token: data.refresh_token })

if (originalRequestConfig.data) {
originalRequestConfig.data = JSON.parse(originalRequestConfig.data)
}

originalRequestConfig.headers = { 'Authorization': `Bearer ${data.token}` };
api.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;

failedQueue.forEach(request => {
request.onSuccess(data.token)
})

resolve(api(originalRequestConfig))

} catch (error: any) {
failedQueue.forEach(request => {
request.onFailure(error)
})

signOut();
reject(error);
} finally {
isRefreshing = false;
failedQueue = [];
}
})

}

signOut();
}





if (requestError.response && requestError.response.data) {
return Promise.reject(new AppError(requestError.response.data.message));
} else {
return Promise.reject(requestError)
}
})

return () => {
api.interceptors.response.eject(interceptTokenManager);
}

}



export { api }

Contexto

import { createContext, ReactNode, useState, useEffect } from "react";

import { storageAuthTokenSave, storageAuthTokenGet, storageAuthTokenRemove } from '@storage/storageAuthToken'
import { storageUserSave, storageUserGet, storageUserRemove } from "@storage/storageUser";

import { api } from "@services/api";
import { UserDTO } from "@dtos/UserDTO";

export type AuthContextDataProps = {
user: UserDTO;
SignIn: (email: string, password: string) => Promise<void>;
updateUserProfile: (serUpdated: UserDTO) => Promise<void>;
isLoadingUserStorageData: boolean;
signOut: () => Promise<void>
}

type AuthContextProviderProps = {
children: ReactNode;
}

export const AuthContext = createContext<AuthContextDataProps>({} as AuthContextDataProps);

export function AuthContextProvider({ children }: AuthContextProviderProps) {
const [user, setUser] = useState<UserDTO>({} as UserDTO)
const [isLoadingUserStorageData, setIsLoadingUserStorageData] = useState(true)

async function userAndTokenUpdate(userData: UserDTO, token: string) {
api.defaults.headers.common['Authorization'] = `Bearer ${token}`
setUser(userData)
}

async function storageUserAndTokenSave(userData: UserDTO, token: string, refresh_token: string) {
try {
setIsLoadingUserStorageData(true)

await storageUserSave(userData)
await storageAuthTokenSave({ token, refresh_token })

} catch (error) {
throw error
} finally {
setIsLoadingUserStorageData(false)
}
}

async function SignIn(email: string, password: string) {
try {

const { data } = await api.post('/sessions', { email, password })

if (data.user && data.token && data.refresh_token) {
setIsLoadingUserStorageData(true)
await storageUserAndTokenSave(data.user, data.token, data.refresh_token)

userAndTokenUpdate(data.user, data.token)
}
} catch (error) {
throw error;
} finally {
setIsLoadingUserStorageData(false)
}
}

async function signOut() {
try {
setIsLoadingUserStorageData(true)
setUser({} as UserDTO)

await storageUserRemove()
await storageAuthTokenRemove()

} catch (error) {
throw error
} finally {
setIsLoadingUserStorageData(false)
}
}

async function updateUserProfile(userUpdated: UserDTO) {
try {
setUser(userUpdated)
await storageUserSave(userUpdated)
} catch (error) {
throw error
}
}

async function loadUserData() {
try {
setIsLoadingUserStorageData(true)

const userLogged = await storageUserGet()
const { token } = await storageAuthTokenGet()

if (token && userLogged) {
userAndTokenUpdate(userLogged, token)

}
} catch (error) {
throw error
} finally {
setIsLoadingUserStorageData(false)
}
}

useEffect(() => {
loadUserData()
}, [])

useEffect(() => {
const subScribe = api.registerInterceptTokenManager(signOut)

return () => {
subScribe()
}
}, [signOut])

return (
<AuthContext.Provider value={{ user, SignIn, isLoadingUserStorageData, signOut, updateUserProfile }}>
{children}
</AuthContext.Provider>
)
}

--

--

Responses (2)