Cómo proteger las API de GraphQL: implementación de la autenticación de usuario en Express.js mediante JWT
Descubra cómo los JWT ofrecen una solución sencilla al complicado problema de la autenticación.
GraphQL es una alternativa popular a la arquitectura API RESTful tradicional, que ofrece un lenguaje de manipulación y consulta de datos flexible y eficiente para las API. Con su creciente adopción, se vuelve cada vez más importante priorizar la seguridad de las API GraphQL para proteger las aplicaciones del acceso no autorizado y posibles violaciones de datos.
Un enfoque eficaz para proteger las API de GraphQL es implementar tokens web JSON (JWT). Los JWT proporcionan un método seguro y eficiente para otorgar acceso a recursos protegidos y realizar acciones autorizadas, garantizando una comunicación segura entre clientes y API.
Autenticación y autorización en las API GraphQL
A diferencia de las API REST, las API GraphQL suelen tener un único punto final que permite a los clientes solicitar dinámicamente diferentes cantidades de datos en sus consultas. Si bien esta flexibilidad es su punto fuerte, también aumenta el riesgo de posibles ataques a la seguridad, como vulnerabilidades de control de acceso rotas.
Para mitigar este riesgo, es importante implementar procesos sólidos de autenticación y autorización, incluida la definición adecuada de permisos de acceso. Al hacerlo, garantiza que solo los usuarios autorizados puedan acceder a los recursos protegidos y, en última instancia, reduce el riesgo de posibles violaciones de seguridad y pérdida de datos.
Puedes encontrar el código de este proyecto en su repositorio de GitHub.
Configurar un servidor Apollo Express.js
Apollo Server es una implementación de servidor GraphQL ampliamente utilizada para las API GraphQL. Puede usarlo para crear fácilmente esquemas GraphQL, definir solucionadores y administrar diferentes fuentes de datos para sus API.
Para configurar un servidor Express.js Apollo, cree y abra una carpeta de proyecto:
mkdir graphql-API-jwt
cd graphql-API-jwt
A continuación, ejecute este comando para inicializar un nuevo proyecto Node.js usando npm, el administrador de paquetes de Node:
npm init --yes
Ahora, instale estos paquetes.
npm install apollo-server graphql mongoose jsonwebtokens dotenv
Por último, cree un archivo server.js en el directorio raíz y configure su servidor con este código:
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);
});
El servidor GraphQL está configurado con los parámetros typeDefs y resolvers, especificando el esquema y las operaciones que la API puede manejar. La opción context configura el objeto req en el contexto de cada solucionador, lo que permitirá al servidor acceder a detalles específicos de la solicitud, como los valores del encabezado.
Crear una base de datos MongoDB
Para establecer la conexión de la base de datos, primero cree una base de datos MongoDB o configure un clúster en MongoDB Atlas. Luego, copie la cadena URI de conexión de la base de datos proporcionada, cree un archivo .env e ingrese la cadena de conexión de la siguiente manera:
MONGO_URI="<mongo_connection_uri>"
Definir el modelo de datos
Defina un modelo de datos usando Mongoose. Cree un nuevo archivo models/user.js e incluya el siguiente código:
const {model, Schema} = require('mongoose');
const userSchema = new Schema({
name: String,
password: String,
role: String
});
module.exports = model('user', userSchema);
Definir el esquema GraphQL
En una API GraphQL, el esquema define la estructura de los datos que se pueden consultar, además de describir las operaciones disponibles (consultas y mutaciones) que puede realizar para interactuar con los datos a través de la API.
Para definir un esquema, cree una nueva carpeta en el directorio raíz de su proyecto y asígnele el nombre graphql. Dentro de esta carpeta, agregue dos archivos: typeDefs.js y resolvers.js.
En el archivo typeDefs.js, incluya el siguiente 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;
Crear solucionadores para la API GraphQL
Las funciones de resolución determinan cómo se recuperan los datos en respuesta a las consultas y mutaciones del cliente, así como otros campos definidos en el esquema. Cuando un cliente envía una consulta o mutación, el servidor GraphQL activa los solucionadores correspondientes para procesar y devolver los datos requeridos de varias fuentes, como bases de datos o API.
Para implementar la autenticación y autorización mediante tokens web JSON (JWT), defina resolutores para las mutaciones de registro e inicio de sesión. Estos se encargarán de los procesos de registro y autenticación de usuarios. Luego, cree un solucionador de consultas de recuperación de datos al que solo podrán acceder usuarios autenticados y autorizados.
Pero primero, defina las funciones para generar y verificar los JWT. En el archivo resolvers.js, comience agregando las siguientes importaciones.
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
Asegúrese de agregar la clave secreta que utilizará para firmar tokens web JSON en el archivo .env.
SECRET_KEY = '<my_Secret_Key>';
Para generar un token de autenticación, incluya la siguiente función, que también especifica atributos únicos para el token JWT, por ejemplo, el tiempo de vencimiento. Además, puede incorporar otros atributos, como emitido en el momento, según los requisitos específicos de su aplicación.
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);
return token;
}
Ahora, implemente la lógica de verificación de tokens para validar los tokens JWT incluidos en solicitudes HTTP posteriores.
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');
}
}
Esta función tomará un token como entrada, verificará su validez utilizando la clave secreta especificada y devolverá el token decodificado si es válido; de lo contrario, arrojará un error que indica un token no válido.
Definir los solucionadores de API
Para definir los solucionadores para la API GraphQL, debe delinear las operaciones específicas que administrará, en este caso, las operaciones de registro e inicio de sesión de usuario. Primero, cree un objeto resolvers que contendrá las funciones de resolución y luego defina las siguientes operaciones de mutación:
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');
}
}
},
La mutación registro maneja el proceso de registro agregando los datos del nuevo usuario a la base de datos. Si bien la mutación login administra los inicios de sesión de los usuarios, si la autenticación es exitosa, generará un token JWT y devolverá un mensaje de éxito en la respuesta.
Ahora, incluya el solucionador de consultas para recuperar datos del usuario. Para garantizar que solo los usuarios autenticados y autorizados puedan acceder a esta consulta, incluya una lógica de autorización para restringir el acceso únicamente a los usuarios con una función de Administrador.
Básicamente, la consulta verificará primero la validez del token y luego la función del usuario. Si la verificación de autorización tiene éxito, la consulta de resolución procederá a buscar y devolver los datos de los usuarios de la base de datos.
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');
}
},
},
};
Finalmente, inicie el servidor de desarrollo:
node server.js
¡Impresionante! Ahora, continúe y pruebe la funcionalidad de la API utilizando el entorno de pruebas de la API del servidor Apollo en su navegador. Por ejemplo, puede utilizar la mutación registro para agregar nuevos datos de usuario en la base de datos, y luego, la mutación iniciar sesión para autenticar al usuario.
Por último, agregue el token JWT a la sección del encabezado de autorización y proceda a consultar la base de datos en busca de datos del usuario.
Proteger las API de GraphQL
La autenticación y la autorización son componentes cruciales para proteger las API de GraphQL. No obstante, es importante reconocer que es posible que por sí solos no sean suficientes para garantizar una seguridad integral. Debe implementar medidas de seguridad adicionales como validación de entradas y cifrado de datos confidenciales.
Al adoptar un enfoque de seguridad integral, puede proteger sus API contra diferentes ataques potenciales.