Segurança e Autenticação
Last updated
Last updated
A autenticação de APIs desempenha um papel crucial na segurança e na integridade dos sistemas modernos, e uma abordagem amplamente adotada para garantir a autenticidade e a autorização de requisições é o uso do JSON Web Tokens (JWT). O JWT é um padrão de token seguro e eficiente que se tornou uma escolha popular para autenticação e autorização em serviços web e aplicativos. Neste artigo, exploraremos em detalhes o funcionamento e a implementação da autenticação de APIs usando JWT, abordando conceitos fundamentais, fluxos de trabalho e melhores práticas para garantir a proteção das suas APIs e a segurança dos seus dados.
Existem dois tipos gerais de token:
Token Transparente: Um token transparente contém informações diretamente legíveis em sua estrutura, geralmente na forma de um JWT (JSON Web Token). Isso significa que todas as informações necessárias para verificar a autenticidade e autorização estão contidas no próprio token.
Token Opaco: Um token opaco, em contraste com um token transparente, é um tipo de token que não contém informações diretamente legíveis em sua estrutura. Em vez disso, ele é uma referência ou identificador que aponta para informações armazenadas de forma segura no servidor de autenticação.
Cabeçalho (Header): A primeira parte de um JWT é o cabeçalho, que é um objeto JSON que descreve o tipo do token e o algoritmo de criptografia utilizado para assiná-lo. Por exemplo, o cabeçalho pode indicar que o token é um JWT e que a assinatura foi gerada usando o algoritmo HMAC SHA256.
Carga Útil (Payload): A segunda parte do JWT é a carga útil, que contém as informações reivindicadas sobre o usuário ou entidade que está autenticando. Essas informações são chamadas de "claims" e podem incluir dados como o ID do usuário, papéis ou funções, informações de expiração e muito mais. Existem três tipos de claims: reivindicações registradas, reivindicações públicas e reivindicações privadas.
Assinatura (Signature): A terceira parte do JWT é a assinatura, que é usada para verificar a integridade do token e garantir que ele não tenha sido adulterado durante a transmissão. A assinatura é gerada usando a chave secreta do servidor de autenticação e os dados do cabeçalho e da carga útil. Quando o receptor do token recebe o JWT, ele verifica a assinatura usando a chave pública do servidor de autenticação. Se a assinatura corresponder, isso indica que o token é válido e não foi modificado.
Geralmente, os tokens são incluídos no cabeçalho HTTP da solicitação, especificamente no cabeçalho "Authorization".
Obtenção do Token: Antes de enviar uma solicitação para o servidor de recursos, o cliente deve obter um token de autenticação válido do servidor de autenticação. Isso geralmente envolve um processo de autenticação, como login ou autenticação de terceiros (por exemplo, usando redes sociais).
Inclusão no Cabeçalho Authorization: O token é então incluído no cabeçalho HTTP da solicitação. Para isso, o cliente adiciona um cabeçalho "Authorization" à solicitação. O valor desse cabeçalho é composto por um tipo de autenticação (como "Bearer") seguido de um espaço e, em seguida, o token em si.
Exemplo: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5
Envio da Solicitação: A solicitação, incluindo o cabeçalho "Authorization", é então enviada para o servidor.
Validação do Token: No servidor, o token é extraído do cabeçalho "Authorization". O servidor, em seguida, valida o token para garantir que seja válido, não tenha expirado e que a solicitação tenha permissão para acessar os recursos desejados.
Processamento da Solicitação: Se o token for válido e autorizado, o servidor de recursos processará a solicitação e responderá de acordo.
O uso do cabeçalho "Authorization" é uma prática padronizada e segura para autenticar solicitações HTTP em APIs. Além disso, essa abordagem facilita a integração com bibliotecas e frameworks de desenvolvimento, uma vez que muitos deles oferecem suporte nativo para a manipulação de tokens no cabeçalho "Authorization".
A primeira melhoria necessária para o projeto é a implementação da criptografia de senhas, fazendo com que elas sejam armazenadas no banco de dados como hashes em vez de texto legível. Isso garante uma camada essencial de segurança aos dados confidenciais dos usuários.
Para isso vamos instalar a biblioteca bcrypt através do comando npm i bcrypt
. Agora podemos criar um novo diretório denominado service e dentro deste diretório criar um arquivo denominado authService.js. Neste arquivo vamos criar uma função denominada hashPassword que será responsável por produzir a hash que iremos armazenar no banco.
Importação do Bcrypt: Iniciamos importando a biblioteca bcrypt
, que é uma biblioteca amplamente utilizada para realizar o hashing seguro de senhas. O bcrypt
é especialmente projetado para proteger senhas contra ataques de força bruta e dicionário, tornando difícil a recuperação da senha original a partir do hash.
hashPassword Function: Esta é a função assíncrona que aceita a senha do usuário como entrada e retorna a senha hasheada. Ela segue os seguintes passos:
a. Geração de um Sal (Salt): O sal é uma sequência aleatória de bytes que é gerada usando o método genSalt
do bcrypt
. O sal é uma parte crucial do processo de hashing seguro, uma vez que torna as senhas hasheadas únicas, mesmo que as senhas originais sejam idênticas. O número 12
passado como argumento para o genSalt
define o custo da geração do sal, ou seja, a quantidade de trabalho necessária para gerar o sal.
b. Geração da Hash da Senha: Em seguida, vamos utilizar o método hash
do bcrypt
para gerar o hash da senha, recebendo como argumentos a senha original e o sal gerado anteriormente. O resultado é a senha hasheada.
c. Retorno do Hashed Password: A função retorna a senha hasheada, que pode ser armazenada no banco de dados de forma segura.
Exportação da Função: A função hashPassword
é exportada como um módulo, tornando-a disponível para ser usada em outros módulos ou partes do projeto.
O uso de um sal (salt) é essencial para a segurança do processo de hashing de senhas. Um sal é uma sequência aleatória que é única para cada senha. Isso significa que, mesmo que dois usuários tenham senhas idênticas, seus hashes serão diferentes devido ao uso de sais diferentes. Isso torna muito mais difícil para atacantes usar tabelas de hash pré-computadas (rainbow tables) ou realizar ataques de força bruta. Além disso, a geração de um sal adiciona uma camada extra de aleatoriedade ao processo de hashing, o que é fundamental para a segurança das senhas armazenadas no banco de dados.
Podemos agora modificar a função adicionarUsuario em usuarioController.js para armazenar a senha como hashe no bd. Para isso vamos fazer o import da função hashPassword do authService no controller e gerar uma hashe antes de armazenar a senha no banco.
Agora quando adicionamos um novo usuário a senha será armazenada como uma hashe.
Agora vamos criar as funções que autenticam o usuário e entregam acces e refresh token.
Token de Acesso (Acces Token):
O token de acesso, frequentemente chamado de "access token", é um token de curta duração que concede permissão a um cliente (geralmente um aplicativo ou serviço) para acessar recursos protegidos em um servidor. Esses recursos podem incluir dados de usuário, serviços da API, ou outras informações restritas.
A principal razão para usar um token de acesso, como o próprio nome sugere, é permitir o acesso a recursos restritos sem exigir que o usuário insira suas credenciais (como usuário e senha) a cada solicitação. Isso melhora a experiência do usuário e evita o compartilhamento de credenciais com aplicativos de terceiros.
Token de Atualização (Refresh Token):
O token de atualização, conhecido como "refresh token," é um token de longa duração usado para obter um novo token de acesso (access token) após o token de acesso expirar. Os tokens de acesso têm um tempo de vida curto por questões de segurança, mas os tokens de atualização podem ser usados para obter tokens de acesso novos e válidos sem a necessidade de autenticação repetida.
O uso de tokens de atualização é crucial para manter a segurança e a praticidade. Como os tokens de acesso têm vida curta, um token de atualização é usado para obter um novo token de acesso sem requerer que o usuário faça login novamente. Isso permite que o cliente mantenha o acesso contínuo aos recursos, enquanto os tokens de atualização são armazenados de forma segura, em um local protegido.
Para gerar os tokens de maneira simples vamos importar a biblioteca jsonwebtoken através do comando npm i jsonwebtoken
. Agora vamos adicionar as funções generateAccessToken e generateRefreshToken no arquivo authService.js da seguinte forma:
Importação de Módulos:
Importando os módulos jsonwebtoken
e dotenv
.
jsonwebtoken é uma biblioteca usada para criar tokens JWT (JSON Web Tokens), que são frequentemente usados para autenticação e autorização em sistemas web.
dotenv é usado para carregar variáveis de ambiente a partir de um arquivo .env
, que é uma prática comum para manter configurações sensíveis e chaves secretas fora do código-fonte.
Obtenção da Chave Secreta:
A chave secreta usada para assinar os tokens é obtida a partir das variáveis de ambiente carregadas pelo dotenv
. Ela é armazenada na variável SECRET_KEY
, que deve ser definida no arquivo .env
para manter a segurança da chave de assinatura.
Função generateAccessToken
:
Esta função é responsável por gerar um token de acesso. Ela aceita um objeto user
como parâmetro, que normalmente conteria informações sobre o usuário autenticado, como o ID, nome e permissões.
O método jwt.sign
é usado para criar o token. Os parâmetros passados para jwt.sign
incluem o payload (as informações a serem incluídas no token), a chave secreta para assinatura e as opções, como o tempo de expiração (expiresIn
).
Neste exemplo, o token de acesso expira após 15 minutos (15m), o que significa que o usuário terá que reautenticar após esse período.
Função generateRefreshToken
:
Esta função gera um token de atualização, que é usado para obter um novo token de acesso após o token de acesso anterior expirar.
Ela é semelhante à função generateAccessToken
e também utiliza o jwt.sign
para criar o token de atualização. Neste caso, o token de atualização expira após 30 minutos (30m).
Exportação das Funções:
As funções hashPassword
, generateAccessToken
, e generateRefreshToken
são exportadas como um módulo, tornando-as disponíveis para serem usadas em outros módulos ou partes do projeto.
Esses tokens são parte essencial de um sistema de autenticação seguro e são usados para garantir que os usuários tenham acesso controlado a recursos protegidos, enquanto os tokens de atualização garantem que os usuários possam renovar seu acesso sem a necessidade de autenticação repetida. A chave secreta (SECRET_KEY
) deve ser protegida e mantida em segurança, pois é usada para assinar os tokens e verificar sua autenticidade.
Vamos agora criar dentro da pasta controller um arquivo authController.js que irá autenticar o usuário e fornecer os respectivos tokens bem como dentro da pasta router um authRouter.js para mapearmos o controller.
Importação de Módulos:
importamos o modelo de usuário (User
) do arquivo userModel
, que contém a definição do modelo de usuário e métodos associados.
Também é importado o módulo bcrypt
, que é usado para verificar a senha fornecida pelo usuário em relação à senha hasheada armazenada no banco de dados.
Além disso, as funções generateAccessToken
e generateRefreshToken
são importadas do módulo authService
, que contém funções para gerar tokens JWT.
Função authenticateUser
:
Esta função é responsável por autenticar um usuário com base nas informações fornecidas no corpo da requisição HTTP, que incluem o campo usuario
e password
O primeiro passo é buscar um usuário no banco de dados que corresponda ao nome de usuário fornecido, usando o método findOne
do Sequelize.
Verificação de Credenciais:
A função verifica se o usuário foi encontrado no banco de dados e, se encontrado, utiliza a função bcrypt.compareSync
para comparar a senha fornecida pelo usuário com a senha hasheada armazenada no banco de dados.
Se as credenciais não coincidirem (usuário não encontrado ou senha incorreta), a função retorna uma resposta de erro com status 401 e a mensagem "Credenciais inválidas".
Geração de Tokens:
Se as credenciais forem verificadas com sucesso, a função chama as funções generateAccessToken
e generateRefreshToken
para criar os tokens de acesso e atualização com base nas informações do usuário recuperadas do banco de dados.
Resposta da Solicitação:
Uma vez que os tokens tenham sido gerados com sucesso, a função responde com um status 200 (OK) e envia o nome do usuário, o token de acesso e o token de atualização em formato JSON na resposta.
Tratamento de Erros:
Se ocorrerem exceções durante a execução da função, como erros de banco de dados, a função captura essas exceções e responde com um status 500 (Erro Interno do Servidor) e a mensagem "Falha ao autenticar".
Mapeamento em authRouter.js:
Agora vamos modificar o arquivo authService.js adicionando as funções responsáveis por validar os tokens.
Função requirePermission(...permission)
:
Esta função é um middleware que verifica se um token de acesso é válido e se o usuário tem as permissões necessárias para acessar uma rota específica. Ela é usada para proteger rotas ou endpoints que requerem permissões específicas.
A função recebe um ou mais argumentos (usando o operador ...
para coletá-los em um array chamado permission
). Esses argumentos representam as permissões necessárias para acessar a rota. Por exemplo, requirePermission("admin", "editor")
exige que o usuário tenha pelo menos uma das duas permissões (admin ou editor).
A função recebe os parâmetros req
(requisição), res
(resposta) e next
(função para passar a requisição para o próximo middleware ou controlador na cadeia de middleware).
Ela extrai o token de acesso do cabeçalho de autorização da requisição, remove a palavra "Bearer" que geralmente precede o token e, em seguida, decodifica o token usando a chave de assinatura (SECRET_KEY
).
Se o token não estiver presente, a função responde com um status 401 (Não Autorizado) e uma mensagem informando que o token de acesso não foi fornecido.
Se o token estiver presente, mas for inválido, a função responde com um status 401 e uma mensagem informando que o token de acesso é inválido.
Se o token for válido, a função verifica se o usuário possui permissões que correspondem a pelo menos uma das permissões passadas como argumentos. Se não houver correspondência, a função responde com um status 403 (Proibido), indicando que o acesso não é autorizado.
Se o token for válido e o usuário possuir as permissões adequadas, o token decodificado é armazenado na requisição (req.user
) para ser acessado posteriormente e, em seguida, a função chama next()
, permitindo que a requisição prossiga para o controlador da rota.
Função validadeRefresh(refreshToken)
:
Esta função verifica se um token de atualização (refresh token) é válido. Os tokens de atualização são usados para obter novos tokens de acesso após a expiração do token anterior.
A função recebe o token de atualização como argumento e tenta decodificá-lo usando a chave de assinatura (SECRET_KEY
).
Se o token de atualização for válido, a função retorna o token decodificado. Caso contrário, ela retorna null
.
Exportação de Funções:
As funções hashPassword
, generateAccessToken
, generateRefreshToken
, requirePermission
e validadeRefresh
são exportadas como um módulo, tornando-as disponíveis para serem usadas em outros módulos ou partes do projeto.
Podemos agora bloquear as rotas colocando a função requirePermission como um middleware entre a rota e controller mapeado. Vamo fazer isso no arquivo userRouter.js.
Se o seu projeto não requer a implementação de funções ou permissões distintas para diferentes tipos de usuários, você pode simplificar o processo de autenticação e autorização eliminando a verificação de permissão na função requirePermission
. Nesse caso, a função requirePermission
pode ser ajustada para validar apenas a autenticidade do token de acesso, sem a necessidade de verificar se o usuário possui permissões específicas. Dessa forma, a função se torna uma verificação básica de autenticação, sem a necessidade de considerar papéis ou permissões diferenciados.
Vamos agora adicionar uma função em authController.js que renova os tokens quando um refresh token é fornecido.
Parâmetros e Importações:
A função é projetada para ser usada como um controlador para uma rota específica em um servidor web. Portanto, ela espera receber a requisição (req
) e a resposta (res
) como parâmetros.
Extração do Token de Atualização:
A função extrai o token de atualização do cabeçalho de autorização da requisição, semelhante ao que a função requirePermission
faz.
Verificação do Token de Atualização:
A função chama a função validadeRefresh
para verificar se o token de atualização é válido. Se o token de atualização for válido, validadeRefresh
retorna o token decodificado, que contém informações sobre o usuário.
Geração de Novos Tokens:
Se o token de atualização for válido, a função procura o usuário no banco de dados com base nas informações contidas no token de atualização (por exemplo, o ID do usuário). Isso é feito chamando UserRepository.findByPk(decodedRefresh.id)
.
Em seguida, a função gera novos tokens de acesso e atualização para o usuário com base nas informações recuperadas do banco de dados. Isso é feito chamando as funções generateAccessToken
e generateRefreshToken
com o usuário como argumento.
Resposta da Solicitação:
Se o token de atualização for válido e a renovação for bem-sucedida, a função responde com um status 200 (OK) e envia os novos tokens de acesso e atualização em formato JSON na resposta.
Tratamento de Erros:
Se ocorrerem exceções durante a execução da função, como erros de banco de dados ou problemas com o token de atualização, a função captura essas exceções e responde com um status 401 (Não Autorizado) ou 500 (Erro Interno do Servidor), dependendo da natureza do erro.
Exportação das Funções:
A função authenticateUser
e a função renovarTokens
são exportadas como um módulo, tornando-as disponíveis para serem usadas em outros módulos ou partes do projeto.
Podemos agora mapear este controller em authRouter:
Agora vamos finalizar importando todos os routers para o server fazendo com que as rotas fiquem disponíveis. Vamos também criar um usuário admin padrão quando o servidor iniciar.
Função criaAdmin
:
Esta função é projetada para ser executada como um utilitário ou função de inicialização. Ela não recebe parâmetros da requisição ou resposta, pois é usada para realizar uma tarefa específica, que é verificar a existência de um usuário administrativo no sistema e, se necessário, criar esse usuário.
Bloco try
:
O código usa um bloco try
para capturar e lidar com exceções que possam ocorrer durante a execução da função.
Consulta ao Banco de Dados:
A função utiliza o método findOrCreate
do Sequelize para verificar a existência de um usuário com o ID igual a 1 no banco de dados. O método findOrCreate
tentará encontrar um registro que atenda aos critérios especificados (neste caso, um usuário com nome “root”), e se não encontrar, criará um novo registro com os valores especificados em defaults
.
Desestruturação da Resposta:
A função findOrCreate
retorna um array com dois elementos: o primeiro é o objeto que representa o usuário (encontrado ou criado), e o segundo é um valor booleano (true
se o usuário foi criado, false
se o usuário já existia).
O código utiliza a desestruturação para extrair esses dois valores nas variáveis user
e created
. Isso permite verificar se o usuário foi criado ou se já existia.
Verificação de Criação:
A função verifica se o usuário foi criado ou se já existia. Se created
for true
, isso significa que o usuário foi criado com sucesso e exibe uma mensagem indicando que as linhas foram criadas com sucesso. Se created
for false
, isso significa que o usuário já existia na tabela e exibe uma mensagem informando que as linhas já existem.
Tratamento de Erros:
Se ocorrerem exceções durante a consulta ao banco de dados ou qualquer outra parte da função, o bloco catch
capturará a exceção e exibirá detalhes do erro no console.
Observe o server.js final abaixo:
Agora caso tentarmos criar um usuário sem realizar o login o sistema devolve uma mensagem solicitando o token.
Devemos então realizar login com um admin para criarmos um usuário
Podemos agora copiar(sem as aspas) o token de acesso e colar no campo Authorization selecionando Bearer Token.
Ao fornecer o token passa a ser possível a criação do usuário Maria.