Como proteger APIs GraphQL: Implementando a autenticação de usuário no Express.js usando JWTs
O GraphQL é uma alternativa popular à arquitetura tradicional da API RESTful, oferecendo uma linguagem de consulta e manipulação de dados flexível e eficiente para APIs. Com sua crescente adoção, torna-se cada vez mais importante priorizar a segurança das APIs GraphQL para proteger os aplicativos contra acesso não autorizado e possíveis violações de dados.
Uma estratégia viável para proteger as APIs GraphQL envolve a utilização de JSON Web Tokens (JWTs), que oferecem um meio eficaz de conceder acesso a recursos restritos e executar operações privilegiadas, mantendo a comunicação segura entre clientes e APIs.
Autenticação e autorização em APIs GraphQL
Em contraste com as interfaces de programação de aplicativos (APIs) RESTful, as implementações de API GraphQL geralmente apresentam um ponto de extremidade único que permite que os clientes solicitem diversas quantidades e tipos de informações por meio de suas consultas. Esta adaptabilidade representa tanto uma vantagem como uma potencial fonte de riscos de segurança, particularmente no que diz respeito a mecanismos de controlo de acesso comprometidos.
Para gerir eficazmente a ameaça acima referida, é crucial estabelecer protocolos de autenticação e autorização abrangentes, o que inclui a definição meticulosa dos privilégios de acesso. Através destas medidas, garantimos que apenas os indivíduos autorizados têm acesso a activos sensíveis, minimizando assim a probabilidade de vulnerabilidades de segurança e incidentes de fuga de dados.
[inserir aqui o link do repositório GitHub].
Configurar um servidor Apollo Express.js
O servidor Apollo é uma implementação de servidor GraphQL amplamente usada para APIs GraphQL. Pode utilizá-lo para criar facilmente esquemas GraphQL, definir resolvedores e gerir diferentes fontes de dados para as suas APIs.
Para estabelecer um servidor Apollo Express.js, é necessário criar e abrir um diretório de projeto.
mkdir graphql-API-jwt
cd graphql-API-jwt
Para iniciar um novo projeto Node.js utilizando o Node Package Manager (npm), execute o seguinte comando:
npm init --yes
Agora, instale estes pacotes.
npm install apollo-server graphql mongoose jsonwebtokens dotenv
De facto, é essencial gerar um ficheiro server.js
na pasta principal do projeto e configurar o servidor utilizando o script fornecido da seguinte forma:
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();
const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({ req }),
});
const MONGO_URI = process.env.MONGO_URI;
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connected to DB");
return server.listen({ port: 5000 });
})
.then((res) => {
console.log(`Server running at ${res.url}`);
})
.catch(err => {
console.log(err.message);
});
O servidor GraphQL foi configurado usando uma combinação de typeDefs
e resolvers
, que definem o esquema e as operações que a API é capaz de manipular.Além disso, a utilização da opção context
permite a configuração do objeto req
para incluir o contexto específico relevante para cada resolvedor individual. Isto permite ao servidor aceder a informações específicas do pedido, incluindo valores de cabeçalho, de modo a facilitar uma recuperação de dados mais eficiente.
Criar um banco de dados MongoDB
criar um novo banco de dados na instalação local do MongoDB ou utilizar a solução baseada em nuvem oferecida pelo MongoDB Atlas, configurando um cluster e obtendo as informações de conexão necessárias. Depois de obter a cadeia URI de ligação apropriada, pode então criar um novo ficheiro .env
e introduzir os detalhes da ligação no formato especificado.
MONGO_URI="<mongo_connection_uri>"
Definir o modelo de dados
Claro, aqui está um exemplo de como definir um modelo de dados para usuários em um arquivo de esquema do Mongoose chamado models/user.js
:javascriptconst mongoose = require(‘mongoose’);// Definir o esquema de usuárioaconst userSchema = new mongoose.Schema({name: { type: String, required: true },email: { type: String, unique: true, required: true },password: { type: String, required: true },});// Compilar o esquema em um modeloconst User = mongoose.model(‘User’, userSchema);module.exports = User;Neste exemplo, primeiro importamos a biblioteca Mongoose no topo do arquivo. Em seguida, criamos um novo objeto Schema
const {model, Schema} = require('mongoose');
const userSchema = new Schema({
name: String,
password: String,
role: String
});
module.exports = model('user', userSchema);
Definir o esquema GraphQL
O esquema da API GraphQL estabelece a disposição das informações que podem ser recuperadas, além de delinear as possíveis operações (consultas e alterações) que podem ser executadas para se comunicar com os dados por meio da referida interface.
Para estabelecer um esquema para o seu projeto, deve começar por criar uma nova pasta no diretório principal da sua empresa, que será designada por “graphql”. Esta pasta em particular servirá como um enclave onde todos os componentes relacionados com as operações GraphQL são organizados. Dentro dos limites da pasta “graphql”, devem existir dois documentos distintos - um chamado “typeDefs.js”, e outro intitulado “resolvers.js”.
No ficheiro typeDefs.js
, incorpore o seguinte código:
const { gql } = require("apollo-server");
const typeDefs = gql`
type User {
id: ID!
name: String!
password: String!
role: String!
}
input UserInput {
name: String!
password: String!
role: String!
}
type TokenResult {
message: String
token: String
}
type Query {
users: [User]
}
type Mutation {
register(userInput: UserInput): User
login(name: String!, password: String!, role: String!): TokenResult
}
`;
module.exports = typeDefs;
Criar resolvedores para a API GraphQL
As funções do resolvedor desempenham um papel essencial na determinação de como os dados são obtidos em resposta a solicitações de informações e modificações do cliente, incluindo quaisquer detalhes adicionais especificados no esquema.Ao receber uma solicitação de um cliente, o servidor GraphQL ativa os resolvedores relevantes para obter dados de várias fontes, incluindo bancos de dados e serviços externos, e os retorna de acordo.
Para permitir a autenticação e o controlo de acesso utilizando JSON Web Token (JWT) no nosso servidor GraphQL, temos primeiro de estabelecer resolvedores para as mutações “register” e “login”. Esses resolvedores são responsáveis por gerenciar os procedimentos de criação e verificação de contas. Posteriormente, devemos desenvolver um resolvedor de consulta de obtenção de dados que só pode ser acedido por utilizadores registados e verificados a quem tenha sido concedida permissão para o fazer.
Codificador e descodificador RSA JWE do pacote “node-jose”, bem como o módulo “crypto” fornecido nativamente pelo Node.js. Isto permitir-nos-á realizar operações criptográficas relacionadas com a geração e validação de JWTs sem problemas. Deste modo, podemos garantir uma comunicação segura entre a nossa aplicação e os seus utilizadores, mantendo a simplicidade e a facilidade de utilização tanto para os programadores como para os utilizadores finais.
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
Certifique-se de que inclui a chave secreta necessária para assinar JSON Web Tokens no seu ficheiro .env, de modo a utilizá-lo corretamente.
SECRET_KEY = '<my_Secret_Key>';
Para produzir um token de autenticação válido, é essencial utilizar a função fornecida e especificar características personalizadas, incluindo o tempo de expiração, no token JWT. Além disso, podem ser incluídos atributos adicionais, como a hora de emissão, com base nas necessidades específicas da aplicação.
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);
return token;
}
Para garantir a autenticidade dos JSON Web Tokens (JWTs) utilizados em futuras trocas HTTP, é necessário incorporar um processo de validação para estes tokens. Isto será conseguido através da implementação de um mecanismo de autenticação que verifica e confirma a legitimidade de cada JWT apresentado durante essas interacções.
function verifyToken(token) {
if (!token) {
throw new Error('Token not provided');
}
try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
return decoded;
} catch (err) {
throw new Error('Invalid token');
}
}
A função dada aceita um token como entrada, utilizando a chave secreta designada para validar a sua autenticidade. Se o token se revelar legítimo, a função devolve o token desencriptado; no entanto, em caso de discrepâncias, levanta uma exceção que indica a presença de um token inválido.
Definir os resolvedores da API
Para delinear o processo de resolução da nossa API GraphQL, temos de especificar as tarefas ou acções específicas que irá realizar, tais como a gestão dos processos de registo e login dos utilizadores. Inicialmente, estabeleça um objeto que contenha funções de resolução que sirvam para realizar essas tarefas designadas.Posteriormente, implemente as seguintes mutações para facilitar estas operações:
const resolvers = {
Mutation: {
register: async (_, { userInput: { name, password, role } }) => {
if (!name || !password || !role) {
throw new Error('Name password, and role required');
}
const newUser = new User({
name: name,
password: password,
role: role,
});
try {
const response = await newUser.save();
return {
id: response._id,
...response._doc,
};
} catch (error) {
console.error(error);
throw new Error('Failed to create user');
}
},
login: async (_, { name, password }) => {
try {
const user = await User.findOne({ name: name });
if (!user) {
throw new Error('User not found');
}
if (password !== user.password) {
throw new Error('Incorrect password');
}
const token = generateToken(user);
if (!token) {
throw new Error('Failed to generate token');
}
return {
message: 'Login successful',
token: token,
};
} catch (error) {
console.error(error);
throw new Error('Login failed');
}
}
},
A mutação de registo é responsável pelo tratamento do processo de registo através da adição de novos dados de utilizador à base de dados. Por outro lado, a mutação login trata dos logins dos utilizadores e, após uma autenticação bem sucedida, gera um JSON Web Token (JWT), devolvendo também uma mensagem de sucesso na resposta.
Para limitar o acesso a uma consulta que recupera dados do utilizador exclusivamente a utilizadores autenticados e privilegiados que possuam uma função “Admin”, é essencial incorporar a lógica de autorização no sistema. Isto assegura que indivíduos não autorizados não conseguem obter acesso a informação sensível.
Essencialmente, o inquérito irá inicialmente validar a legitimidade do token, seguido de uma avaliação do papel do utilizador. No caso de o exame da autorização ser favorável, o pedido de resolução recuperará e fornecerá as informações dos utilizadores finais a partir do repositório.
Query: {
users: async (parent, args, context) => {
try {
const token = context.req.headers.authorization || '';
const decodedToken = verifyToken(token);
if (decodedToken.role !== 'Admin') {
throw new ('Unauthorized. Only Admins can access this data.');
}
const users = await User.find({}, { name: 1, _id: 1, role:1 });
return users;
} catch (error) {
console.error(error);
throw new Error('Failed to fetch users');
}
},
},
};
Por fim, inicie o servidor de desenvolvimento:
node server.js
De facto, é altamente recomendável que valide a operacionalidade da nossa API GraphQL utilizando a sandbox da API do Servidor Apollo no seu navegador Web. Para ilustrar este processo, considere a utilização da mutação “register” para introduzir novas informações de utilizador na base de dados, seguida da mutação “login” para efeitos de autenticação.
Por fim, incorpore o token JWT no parâmetro do cabeçalho de autorização e, em seguida, recupere as informações do usuário do banco de dados executando uma consulta.
Protegendo as APIs GraphQL
A autenticação e a autorização desempenham um papel fundamental para garantir a segurança das APIs GraphQL; no entanto, sua implementação por si só não garante proteção completa. É essencial incorporar outras medidas de segurança, como a validação robusta de entradas e a encriptação de informações sensíveis, para obter uma segurança completa.
A implementação de uma estratégia de segurança abrangente é essencial para proteger as suas interfaces de programação de aplicações (API) de vários tipos de ameaças que possam surgir.