Como implementar o controle de acesso baseado em função em APIs REST do Express.js usando Passport.js e JWT
O controlo de acesso baseado em funções (RBAC) é uma medida de segurança avançada que permite restringir o acesso a recursos designados apenas a indivíduos com funções específicas, assegurando assim uma maior proteção e confidencialidade.
A utilização desta forma de autenticação permite que os administradores do sistema atribuam permissões de acordo com as designações das funções do utilizador. Este grau de controlo específico confere uma salvaguarda adicional, permitindo que as aplicações obstruam a entrada não aprovada.
Implementação de um mecanismo de controlo de acesso baseado em funções utilizando Passport.js e JWTs
O controlo de acesso baseado em funções (RBAC), que é uma abordagem amplamente utilizada para impor limitações de acesso em aplicações de software em virtude das funções e autorizações dos utilizadores, pode ser executado através de várias técnicas diferentes.
Dois métodos predominantes implicam a utilização de bibliotecas RBAC especializadas, como AccessControl,
A utilização de JSON Web Tokens (JWTs) garante a confidencialidade e a integridade das informações de autenticação durante a transmissão, com Passport.js facilitando o processo de autenticação ao oferecer um middleware de autenticação versátil.
Utilizando este método, é possível atribuir tarefas aos utilizadores e incorporar estas designações no JWT durante a autenticação. Posteriormente, o JWT pode ser utilizado para confirmar a identidade e as responsabilidades do utilizador em solicitações subsequentes, facilitando assim a autorização baseada em funções e a restrição de acesso.
A seleção de qualquer uma das abordagens para a implementação do controlo de acesso baseado em funções (RBAC) depende das necessidades específicas do projeto, uma vez que ambos os métodos possuem benefícios inerentes que os tornam uma solução eficaz.
O código fonte deste projeto está acessível através do seu repositórioGitHub.
Configurar um projeto Express.js
No início do processo, inicie uma instalação local de um projeto Express.js, após o que é aconselhável proceder à adição dos pacotes necessários através de meios remotos.
npm install cors dotenv mongoose cookie-parser jsonwebtoken mongodb \
passport passport-local
O passo seguinte é criar uma base de dados MongoDB ou estabelecer um cluster através do MongoDB Atlas. Uma vez que isso tenha sido feito, copie o URI de conexão do banco de dados e adicione-o a um arquivo .env localizado no diretório raiz do projeto.
CONNECTION_URI="connection URI"
Configurar a ligação à base de dados
No diretório principal, gere um novo ficheiro utils/db.js e adicione o código subsequente para autenticar a associação com a agregação MongoDB em execução no Atlas através da utilização do Mongoose.
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.CONNECTION_URI);
console.log("Connected to MongoDB!");
} catch (error) {
console.error("Error connecting to MongoDB:", error);
}
};
module.exports = connectDB;
Definir o modelo de dados
No diretório raiz, crie um novo ficheiro intitulado “model/user.model.js” e, neste ficheiro, incorpore o código necessário para estabelecer um esquema de dados para as informações do utilizador utilizando o Mongoose.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: String,
password: String,
role: String
});
module.exports = mongoose.model('User', userSchema);
Criar o controlador para os pontos finais da API
Primeiro, faça estas importações:
const User = require('../models/user.model');
const passport = require('passport');
const { generateToken } = require('../middleware/auth');
require('../middleware/passport')(passport);
Desenvolver uma estrutura lógica para lidar com os processos de registo e autenticação de utilizadores.
exports.registerUser = async (req, res) => {
const { username, password, role } = req.body;
try {
await User.create({ username, password, role });
res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
console.log(error);
res.status(500).json({ message: 'An error occurred!' });
}
};
exports.loginUser = (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
console.log(err);
return res.status(500).json({
message: 'An error occurred while logging in'
});
}
if (!user) {
return res.status(401).json({
message: 'Invalid login credentials'
});
}
req.login(user, { session: false }, (err) => {
if (err) {
console.log(err);
return res.status(500).json({
message: 'An error occurred while logging in'
});
}
const { _id, username, role } = user;
const payload = { userId: _id, username, role };
const token = generateToken(payload);
res.cookie('token', token, { httpOnly: true });
return res.status(200).json({ message: 'Login successful' });
});
})(req, res, next);
};
O objetivo da função registerUser é facilitar o registo de um novo utilizador, recebendo as informações necessárias, como o nome de utilizador, a palavra-passe e a função, através de um pedido efectuado no protocolo HTTP. Posteriormente, esta função criará uma nova entrada para o utilizador na base de dados e enviará uma mensagem de confirmação indicando que o registo foi bem sucedido ou notificará quaisquer erros que possam ter ocorrido durante o processamento do pedido.
A função
loginUser
serve para permitir o início de sessão do utilizador através da implementação do mecanismo de autenticação local fornecido pelo Passport.js. Esta funcionalidade verifica as credenciais do utilizador e gera um token após uma autenticação bem sucedida, que é subsequentemente guardado como um cookie para pedidos de autenticação posteriores. Caso surja algum problema durante o processo de início de sessão, será emitida uma notificação adequada.
Por fim, incorpore o código que executa a funcionalidade de recuperação de todos os dados do utilizador a partir da base de dados. Isto servirá como um ponto de acesso limitado, acessível apenas por utilizadores autorizados com o privilégio de um administrador.
exports.getUsers = async (req, res) => {
try {
const users = await User.find({});
res.json(users);
} catch (error) {
console.log(error);
res.status(500).json({ message: 'An error occurred!' });
}
};
Configurar uma estratégia de autenticação local Passport.js
Para garantir a veracidade dos utilizadores que forneceram os seus dados de login, é necessário implementar um protocolo de autenticação local.
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/user.model');
module.exports = (passport) => {
passport.use(
new LocalStrategy(async (username, password, done) => {
try {
const user = await User.findOne({ username });
if (!user) {
return done(null, false);
}
if (user.password !== password) {
return done(null, false);
}
return done(null, user);
} catch (error) {
return done(error);
}
})
);
};
O presente código engloba a criação de um mecanismo de autenticação personalizado para o Passport.js, concebido para verificar a identidade dos utilizadores através do seu nome de utilizador especificado e da sua palavra-passe confidencial.
Ao receber uma combinação de nome de utilizador e palavra-passe do utilizador, o sistema recupera imediatamente os dados correspondentes do utilizador na base de dados. Posteriormente, verifica a exatidão da palavra-passe fornecida, comparando-a com o valor de hash armazenado na base de dados para essa conta de utilizador específica. Se as palavras-passe coincidirem, é gerado um token de autenticação que é devolvido ao utilizador como prova da sua identidade.
Criar um middleware de verificação JWT
Na pasta “middleware”, crie um novo ficheiro intitulado “auth.js” e incorpore o seguinte código para estabelecer e validar JSON Web Tokens (JWTs).
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
const generateToken = (payload) => {
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
return token;
};
const verifyToken = (requiredRole) => (req, res, next) => {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Invalid token' });
}
req.userId = decoded.userId;
if (decoded.role !== requiredRole) {
return res.status(403).json({
message: 'You do not have the authorization and permissions to access this resource.'
});
}
next();
});
};
module.exports = { generateToken, verifyToken };
A função
generateToken
é utilizada para criar um JSON Web Token (JWT) com um tempo de expiração pré-determinado, enquanto a função
verifyToken
verifica se o token existe e é legítimo. Além disso, também confirma que o token desencriptado inclui a função necessária, concedendo assim acesso exclusivamente aos utilizadores que possuem a função e os privilégios autorizados.
Para garantir que os JSON Web Tokens (JWTs) sejam distinguíveis uns dos outros, uma chave secreta distinta deve ser gerada e adicionada ao arquivo .env, conforme demonstrado abaixo:
SECRET_KEY="This is a sample secret key."
Definir as rotas da API
No diretório principal, crie uma nova subpasta chamada “routes” e, neste repositório, introduza um novo ficheiro intitulado “userRoutes.js”. Depois, por favor, cole o seguinte código dentro do ficheiro “userRoutes.js” acima mencionado:
const express = require('express');
const router = express.Router();
const userControllers = require('../controllers/userController');
const { verifyToken } = require('../middleware/auth');
router.post('/api/register', userControllers.registerUser);
router.post('/api/login', userControllers.loginUser);
router.get('/api/users', verifyToken('admin'), userControllers.getUsers);
module.exports = router;
O presente código delineia as rotas HTTP para uma arquitetura RESTful. Entre elas, a rotausers serve como o caminho protegido. Ao restringir a admissão a esta rota apenas a pessoas com o papeladmin, torna-se possível implementar eficazmente a restrição de acesso baseada em papéis.
Atualizar o ficheiro do servidor principal
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const app = express();
const port = 5000;
require('dotenv').config();
const connectDB = require('./utils/db');
const passport = require('passport');
require('./middleware/passport')(passport);
connectDB();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());
app.use(cookieParser());
app.use(passport.initialize());
const userRoutes = require('./routes/userRoutes');
app.use('/', userRoutes);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Em conclusão, inicie a execução do servidor de desenvolvimento para operar a aplicação.
node server.js
Utilizar o mecanismo RBAC para elevar os sistemas de autenticação
A implementação do controlo de acesso baseado em funções é uma estratégia valiosa para garantir a segurança das aplicações, restringindo o acesso com base nas funções e responsabilidades dos utilizadores.
Embora a utilização de bibliotecas de autenticação pré-existentes para o estabelecimento de um sistema RBAC eficaz seja louvável, a implementação de bibliotecas RBAC para delinear explicitamente as funções dos utilizadores e atribuir permissões confere um mecanismo de segurança mais formidável, levando a uma maior proteção da sua aplicação.