Autenticação JWT em Node.js: Guia Completo com Express & MongoDB

A autenticação e a autorização são elementos fundamentais da segurança em sistemas computacionais. Ao utilizar credenciais, como nome de usuário e senha, você comprova sua identidade como um usuário registrado, obtendo assim privilégios adicionais.

Este princípio se estende ao login em serviços online através de contas como Facebook ou Google.

Este artigo tem como objetivo guiar você na criação de uma API Node.js com autenticação JWT (JSON Web Tokens). As ferramentas que utilizaremos neste tutorial são:

  • Express.js
  • Banco de dados MongoDB
  • Mongoose
  • Dotenv
  • Bcrypt.js
  • Jsonwebtoken

Autenticação vs. Autorização

O Que é Autenticação?

A autenticação é o processo de verificar a identidade de um usuário através da apresentação de credenciais, como endereço de e-mail, senha e tokens. As credenciais fornecidas são comparadas com as informações do usuário cadastradas, que podem estar armazenadas em um arquivo do sistema local ou em um banco de dados. Se houver correspondência entre as credenciais fornecidas e os dados no banco de dados, a autenticação é bem-sucedida, e o usuário ganha acesso aos recursos.

O Que é Autorização?

A autorização ocorre após a autenticação. Todo processo de autorização pressupõe uma autenticação prévia. Ela consiste em dar permissão aos usuários para acessar os recursos de um sistema ou site. Neste tutorial, concederemos aos usuários autenticados a permissão para acessar dados de outros usuários. Se um usuário não estiver logado, ele não terá acesso a esses dados.

Um bom exemplo de autorização são as plataformas de redes sociais como o Facebook e o Twitter. Não é possível acessar o conteúdo sem ter uma conta.

Outro exemplo de autorização é o conteúdo por assinatura: embora você possa se autenticar no site, você só terá acesso ao conteúdo depois de se inscrever.

Pré-requisitos

Para acompanhar este tutorial, é necessário ter um conhecimento básico de JavaScript e MongoDB, além de um bom entendimento de Node.js.

Certifique-se de ter o Node.js e o npm instalados em seu computador. Para verificar a instalação, abra o prompt de comando e digite `node -v` e `npm -v`. Você deverá ver um resultado semelhante ao seguinte:

As versões instaladas podem ser diferentes das mostradas na imagem. O npm é instalado automaticamente com o Node.js. Caso ainda não tenha instalado, você pode obtê-lo no site do NodeJS.

Você também precisará de um IDE (Ambiente de Desenvolvimento Integrado) para escrever o código. Neste tutorial, usaremos o editor de código VS Code. Se você preferir outro, pode usá-lo sem problemas. Se não tiver nenhum IDE instalado, baixe-o do site do Visual Studio, selecionando a versão compatível com o seu sistema.

Configuração do Projeto

Crie uma pasta chamada `nodeapi` em qualquer local do seu computador e abra-a com o VS Code. Abra o terminal do VS Code e inicialize o gerenciador de pacotes do Node.js digitando o seguinte comando:

npm init -y

Certifique-se de estar no diretório `nodeapi`.

Este comando criará um arquivo `package.json`, que conterá todas as dependências que utilizaremos neste projeto.

Agora, vamos instalar todos os pacotes listados anteriormente. Digite e execute o seguinte comando no terminal:

npm install express dotenv jsonwebtoken mongoose bcryptjs

Após a instalação, sua estrutura de arquivos e pastas deverá ser semelhante a esta:

Criação do Servidor e Conexão com o Banco de Dados

Crie um arquivo chamado `index.js` e uma pasta chamada `config`. Dentro da pasta `config`, crie dois arquivos: `conn.js`, para a conexão com o banco de dados, e `config.env`, para declarar as variáveis de ambiente. Insira os códigos abaixo nos respectivos arquivos.

index.js

const express = require('express');
const dotenv = require('dotenv');

//Configure dotenv files above using any other library and files
dotenv.config({path:'./config/config.env'}); 

//Creating an app from express
const app = express();

//Using express.json to get request of json data
app.use(express.json());



//listening to the server
app.listen(process.env.PORT,()=> {
    console.log(`Server is listening at ${process.env.PORT}`);
})

Ao utilizar o dotenv, certifique-se de configurá-lo no seu arquivo `index.js` antes de importar outros arquivos que dependem de variáveis de ambiente.

conn.js

const mongoose = require('mongoose');

mongoose.connect(process.env.URI, 
    { useNewUrlParser: true,
     useUnifiedTopology: true })
    .then((data) => {
        console.log(`Database connected to ${data.connection.host}`)
})

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000

Neste exemplo, estou utilizando o URI do Atlas do MongoDB, mas você também pode usar o `localhost`.

Criação de Modelos e Rotas

Um modelo é um esquema de como seus dados serão estruturados no banco de dados MongoDB e armazenados como documentos JSON. Para criar um modelo, usaremos o schema do Mongoose.

O roteamento se refere a como um aplicativo responde às requisições de um cliente. Utilizaremos o recurso de roteador do Express para definir as rotas.

As rotas geralmente recebem dois argumentos: o primeiro é o caminho da rota, e o segundo, uma função de callback que especifica o que aquela rota deve fazer quando o cliente a requisita.

É possível, também, adicionar um terceiro argumento, que seria uma função de middleware, quando necessário – por exemplo, no processo de autenticação. Como estamos construindo uma API autenticada, vamos utilizar o middleware para autenticar e autorizar os usuários.

Crie duas pastas: `routes` e `models`. Dentro da pasta `routes`, crie um arquivo chamado `userRoute.js` e, dentro da pasta `models`, um arquivo chamado `userModel.js`. Após criar os arquivos, insira os códigos abaixo em cada um.

userModel.js

const mongoose = require('mongoose');

//Creating Schema using mongoose
const userSchema = new mongoose.Schema({
    name: {
        type:String,
        required:true,
        minLength:[4,'Name should be minimum of 4 characters']
    },
    email:{
        type:String,
        required:true,
        unique:true,
    },
    password:{
        type:String,
        required:true,
        minLength:[8,'Password should be minimum of 8 characters']
    },
    token:{
        type:String
    }
})

//Creating models
const userModel = mongoose.model('user',userSchema);
module.exports = userModel;

userRoute.js

const express = require('express');
//Creating express router
const route = express.Router();
//Importing userModel
const userModel = require('../models/userModel');

//Creating register route
route.post('/register',(req,res)=> {

})
//Creating login routes
route.post('/login',(req,res)=> {

})

//Creating user routes to fetch users data
route.get('/user',(req,res)=> {

})

Implementação da Funcionalidade de Rota e Criação de Tokens JWT

O Que é JWT?

JSON Web Tokens (JWT) são uma biblioteca JavaScript utilizada para criar e verificar tokens. Trata-se de um padrão aberto para o compartilhamento de informações entre duas partes – um cliente e um servidor. Utilizaremos duas funções do JWT: uma para criar um novo token (`sign`) e outra para verificar o token (`verify`).

O Que é Bcrypt.js?

Bcrypt.js é uma função de hash criada por Niels Provos e David Mazières. Ela utiliza um algoritmo de hash para codificar a senha. As duas funções mais comuns que usaremos nesse projeto são a `hash`, para gerar um valor de hash, e a função `compare`, para comparar senhas.

Implementação da Funcionalidade de Rota

A função de callback no roteamento recebe três argumentos: requisição (request), resposta (response) e a função `next`. O argumento `next` é opcional e é usado quando precisamos dele. Esses argumentos devem ser passados na ordem: `request`, `response` e `next`. Agora, modifique os arquivos `userRoute.js`, `config.env` e `index.js` com os códigos abaixo:

userRoute.js

//Requiring all the necessary files and libraries
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

//Creating express router
const route = express.Router();
//Importing userModel
const userModel = require('../models/userModel');

//Creating register route
route.post("/register", async (req, res) => {

    try {
        const { name, email, password } = req.body;
        //Check emptyness of the incoming data
        if (!name || !email || !password) {
            return res.json({ message: 'Please enter all the details' })
        }

        //Check if the user already exist or not
        const userExist = await userModel.findOne({ email: req.body.email });
        if (userExist) {
            return res.json({ message: 'User already exist with the given emailId' })
        }
        //Hash the password
        const salt = await bcrypt.genSalt(10);
        const hashPassword = await bcrypt.hash(req.body.password, salt);
        req.body.password = hashPassword;
        const user = new userModel(req.body);
        await user.save();
        const token = await jwt.sign({ id: user._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ 'token': token }).json({ success: true, message: 'User registered successfully', data: user })
    } catch (error) {
        return res.json({ error: error });
    }

})
//Creating login routes
route.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        //Check emptyness of the incoming data
        if (!email || !password) {
            return res.json({ message: 'Please enter all the details' })
        }
        //Check if the user already exist or not
        const userExist = await userModel.findOne({email:req.body.email});
        if(!userExist){
            return res.json({message:'Wrong credentials'})
        }
        //Check password match
        const isPasswordMatched = await bcrypt.compare(password,userExist.password);
        if(!isPasswordMatched){
            return res.json({message:'Wrong credentials pass'});
        }
        const token = await jwt.sign({ id: userExist._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({"token":token}).json({success:true,message:'LoggedIn Successfully'})
    } catch (error) {
        return res.json({ error: error });
    }

})

//Creating user routes to fetch users data
route.get('/user', async (req, res) => {
    try {
        const user  = await userModel.find();
        if(!user){
            return res.json({message:'No user found'})
        }
        return res.json({user:user})
    } catch (error) {
        return res.json({ error: error });  
    }
})

module.exports = route;

Ao utilizar a função async, utilize o bloco try-catch, caso contrário, haverá um erro de rejeição de promessa não tratada.

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
SECRET_KEY = KGGK>HKHVHJVKBKJKJBKBKHKBMKHB
JWT_EXPIRE = 2d

index.js

const express = require('express');
const dotenv = require('dotenv');

//Configure dotenv files above using any other library and files
dotenv.config({path:'./config/config.env'}); 
require('./config/conn');
//Creating an app from express
const app = express();
const route = require('./routes/userRoute');

//Using express.json to get request of json data
app.use(express.json());
//Using routes

app.use('/api', route);

//listening to the server
app.listen(process.env.PORT,()=> {
    console.log(`Server is listening at ${process.env.PORT}`);
})

Criação de Middleware para Autenticar o Usuário

O Que é Middleware?

Middleware é uma função que tem acesso ao objeto de requisição, resposta e à próxima função no ciclo de requisição-resposta. A função `next` é chamada quando a função atual termina sua execução. Conforme mencionado acima, use `next()` quando você precisar executar outra função de callback ou middleware.

Agora, crie uma pasta chamada `middleware` e, dentro dela, um arquivo chamado `auth.js` com o seguinte código:

auth.js

const userModel = require('../models/userModel');
const jwt = require('jsonwebtoken');
const isAuthenticated = async (req,res,next)=> {
    try {
        const {token} = req.cookies;
        if(!token){
            return next('Please login to access the data');
        }
        const verify = await jwt.verify(token,process.env.SECRET_KEY);
        req.user = await userModel.findById(verify.id);
        next();
    } catch (error) {
       return next(error); 
    }
}

module.exports = isAuthenticated;

Instale a biblioteca `cookie-parser` para habilitar o `cookieParser` no seu aplicativo. Essa biblioteca permite que você acesse o token armazenado no cookie. Sem o `cookieParser` configurado no seu aplicativo Node.js, não é possível acessar os cookies nos cabeçalhos do objeto de requisição. Execute o seguinte comando no terminal:

npm i cookie-parser

Com o `cookie-parser` instalado, configure seu aplicativo modificando o arquivo `index.js` e adicionando o middleware à rota `”/user/”`.

arquivo index.js

const cookieParser = require('cookie-parser');
const express = require('express');
const dotenv = require('dotenv');

//Configure dotenv files above using any other library and files
dotenv.config({path:'./config/config.env'}); 
require('./config/conn');
//Creating an app from express
const app = express();
const route = require('./routes/userRoute');

//Using express.json to get request of json data
app.use(express.json());
//Configuring cookie-parser
app.use(cookieParser()); 

//Using routes
app.use('/api', route);

//listening to the server
app.listen(process.env.PORT,()=> {
    console.log(`Server is listening at ${process.env.PORT}`);
})

userRoute.js

//Requiring all the necessary files and libraries
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const isAuthenticated = require('../middleware/auth');

//Creating express router
const route = express.Router();
//Importing userModel
const userModel = require('../models/userModel');

//Creating user routes to fetch users data
route.get('/user', isAuthenticated, async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: 'No user found' })
        }
        return res.json({ user: user })
    } catch (error) {
        return res.json({ error: error });
    }
})

module.exports = route;

A rota `”/user”` só será acessível quando o usuário estiver logado.

Verificação das APIs no POSTMAN

Antes de verificar as APIs, modifique o arquivo `package.json` adicionando as seguintes linhas de código:

"scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

Você pode iniciar o servidor digitando `npm start`, mas ele será executado apenas uma vez. Para manter o servidor funcionando enquanto você altera os arquivos, use o nodemon. Para instalá-lo, execute o seguinte comando no terminal:

npm install -g nodemon

O flag `-g` fará com que o nodemon seja instalado globalmente em seu sistema, não sendo necessário instalá-lo repetidamente para cada projeto novo.

Para executar o servidor, digite `npm run dev` no terminal. Você deverá ver o seguinte resultado:

Com o código completo e o servidor em execução, abra o Postman para verificar se tudo está funcionando.

O Que é o Postman?

Postman é uma ferramenta de software para criar, projetar, desenvolver e testar APIs.

Se você não tiver o Postman instalado, baixe-o do site do Postman.

Abra o Postman e crie uma coleção chamada `nodeAPItest` e, dentro dela, crie três requisições: `register`, `login` e `user`. Você deverá ter os seguintes arquivos:

Ao enviar dados JSON para `localhost:5000/api/register`, você deverá obter o seguinte resultado:

Como também estamos criando e salvando tokens em cookies durante o registro, você deverá obter os detalhes do usuário ao solicitar a rota `localhost:5000/api/user`. Você pode verificar as outras requisições no Postman.

Se quiser o código completo, você pode obtê-lo no meu repositório do GitHub.

Conclusão

Neste tutorial, aprendemos como implementar a autenticação em uma API NodeJS utilizando tokens JWT e como autorizar os usuários a acessar dados de outros usuários.

Bons estudos e codificação!