Skip to main content

Cómo crear una API GraphQL segura con Node.js

Escrito por
Headshot of Lawrence Eagles

Lawrence Eagles

blog banner node js

29 de marzo de 2022

0 minutos de lectura

GraphQL ofrece seguridad integrada desde el inicio gracias a la validación y la verificación de tipos. Sin embargo, no aborda por completo los problemas de seguridad relacionados con las API. En este artículo, aprenderemos a proteger las API GraphQL creando una aplicación sencilla de Node.js con Fastify y GraphQL.

Según su documentación oficial, GraphQL es un lenguaje de consulta de grafos para API y un entorno de ejecución que permite procesar esas consultas con nuestros datos. GraphQL describe con claridad los datos de nuestra API y nos permite crear API rápidas y flexibles, a la vez que les da a los clientes control total para describir los datos que necesitan. Al igual que REST, GraphQL funciona sobre HTTP, por lo que no depende de una base de datos y es compatible con cualquier lenguaje de backend o cliente.

Fastify es un framework de Node.js basado en complementos, muy eficiente y de alto rendimiento, ideal para crear servidores HTTP rápidos. Inspirado en Hapi y Express, Fastify ofrece una alternativa más amigable para los desarrolladores y con mejor rendimiento, además de una baja sobrecarga.

Fastify es compatible con GraphQL mediante el complemento Mercurius. Mercurius es un adaptador de GraphQL configurable para Fastify; conoceremos más sobre él en las siguientes secciones.

Empecemos con los requisitos previos.

Requisitos previos

Estos son los requisitos previos para este artículo:

  • Node.js versión 12 o posterior

  • Conocimientos básicos de JavaScript

  • Conocimientos básicos de GraphQL

Primeros pasos

Para empezar, debemos crear un servidor básico de Node.js.

Crea un proyecto inicial con una carpeta para el proyecto y, desde esa carpeta, ejecuta el siguiente código en la interfaz de línea de comandos (CLI). Esto iniciará nuestra aplicación e instalará las dependencias necesarias.

// bootstrap npm project
npm init -y

// install dependencies
npm i fastify nodemon fastify-plugin mercurius-auth jsonwebtoken

Este proyecto usa una versión específica de Mercurius. Para instalarla, ejecuta el siguiente comando:

npm i mercurius@7.9.1

A continuación, habilitamos los módulos ES6 para usar el sistema estándar de módulos de JavaScript en lugar de commonJS. Para ello, agregamos "type": "module" al archivo package.json.

Luego, actualizamos los scripts de NPM con un comando para iniciar el servidor de Node.js. Para ello, abrimos el archivo package.json y editamos la sección de scripts de la siguiente manera:

"scripts": {
    // start the Node.js server in production
    "start": "node --es-module-specifier-resolution=node ./src/index.js",
    // use nodemon to restart development server when code is compiled
    "dev": "nodemon --es-module-specifier-resolution=node ./src/index.js"
}

Ten en cuenta que el fragmento --es-module-specifier-resolution=node es necesario para habilitar la interoperabilidad entre los módulos ES y los módulos commonJS de Node.

Ahora, crea un directorio src en el directorio raíz. Dentro de src, crea una carpeta graphql que contenga los archivos schema.js y resolvers.js. Agrega el siguiente código al archivo schema.js:

const schema =`
  type Query {
    users: [User]!
  }

  type User {
    id: ID!
  }
  `;
  export default schema;

Ahora, agrega el siguiente código al archivo resolvers.js:

const resolvers = {};
export default resolvers;

Actualizaremos schema.js y agregaremos el archivo resolvers.js en la siguiente sección. Pero debemos crearlos ahora con algo de código básico, ya que son necesarios para que nuestro servidor funcione correctamente.

En el directorio src, crea un archivo index.js con el siguiente código:

import fastify from 'fastify';
import mercurius from 'mercurius';
import jwt from 'jsonwebtoken';
import mercuriusAuth from 'mercurius-auth';
import schema from './graphql/schema.js'; 
import resolvers from './graphql/resolvers.js';

const port = process.env.PORT || 4500;
const app = fastify({ logger: true });

// Activate plugins below:
app.register(
  mercurius, { 
      schema, 
      resolvers, 
      graphiql: 'playground', 
      queryDepth: 7 
});

// register auth policy

// create server
const start = async () => {
  try {
    await app.listen(port);
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};
start();

El código anterior crea un servidor Fastify principal y registra el complemento Mercurius con estas opciones: schema, resolvers, graphiql y queryDepth.

Ahora podemos iniciar el servidor ejecutando npm run dev. El resultado es el siguiente:

{"level":30,"time":1620202591072,"pid":11775,"hostname":"pc-name","msg":"Server listening at http://127.0.0.1:4500"}

Ahora podemos ver que nuestro servidor funciona. En la siguiente sección, empezaremos a crear nuestras API de blog con GraphQL.

Cómo crear una API de blog segura con Fastify y GraphQL

Hay varias estrategias para proteger una API. Entre ellas se incluyen:

  • Autenticación y autorización: La autenticación consiste en confirmar que un usuario es quien dice ser, mientras que la autorización se relaciona con los permisos. La autenticación determina si un usuario puede iniciar sesión y, después, lo recuerda. La autorización determina qué permisos se asignan a un usuario identificado y regula si puede realizar operaciones como crear, leer, actualizar o eliminar.

  • Ocultar errores: Evitar revelar información exacta sobre un error del servidor para no proporcionar al cliente, sin querer, detalles que podrían exponer las vulnerabilidades del servidor.

  • Límite de profundidad de las consultas: Especificar una profundidad máxima para las consultas de GraphQL. Las consultas profundamente anidadas son peligrosas porque consumen muchos recursos y son costosas de procesar. Como consecuencia, pueden hacer que nuestras API fallen.

  • Sanitización y validación de entradas: Usa técnicas estándar de seguridad web para evitar que los usuarios envíen datos maliciosos. Aprovecharemos la validación integrada de GraphQL en nuestra aplicación.

En este artículo, crearemos y protegeremos nuestras API con las estrategias anteriores mediante Mercurius y el complemento Mercurius Auth.

Para nuestros fines, el complemento Mercurius Auth tiene dos funciones principales. Primero, nos permite definir directivas de autenticación personalizadas en los campos de nuestro esquema. Las directivas de autenticación son cadenas que se usan como identificadores de los campos protegidos de nuestro esquema.

Además, nos permite aplicar políticas de autenticación personalizadas a estos campos protegidos al realizar una solicitud de GraphQL.

Empezaremos creando datos de prueba. En el directorio src, crea una carpeta data que contenga un archivo index.js con el siguiente código:

export default {
    users: [
        { id: 1, username: 'JohnDoe', email: 'John_doe@gmail.com', password: '12345', role: 'admin' },
        { id: 2, username: 'JaneDoe', email: 'Jane_doe@gmail.com', password: '12345', role: 'user' },
        { id: 3, username: 'JoeDoe', email: 'Joe_doe@gmail.com', password: '12345', role: 'user' }
    ]
};

A continuación, configuramos nuestro schema reemplazando el código básico del archivo schema.js por el siguiente:

const schema = `

directive @auth(
    requires: Role = ADMIN,
  ) on OBJECT | FIELD_DEFINITION

  enum Role {
    ADMIN
    USER
  }

type Query {
    user(id: ID!): User! @auth(requires: ADMIN)
    users: [User]! @auth(requires: ADMIN)
    login(username:String!, password:String!): String
}

type User {
    id: ID!
    username: String!
    email: String!
    password: String!
    role: String!
}
`;

export default schema;

En el código anterior, creamos nuestro esquema de GraphQL y definimos directivas de autenticación para los campos user y users. Enseguida, aplicaremos políticas personalizadas a estos campos protegidos.

Ahora agregamos nuestros resolutores reemplazando el código básico del archivo resolvers.js por el siguiente:

import jwt from 'jsonwebtoken';
import Data from '../data';

const resolvers = {
    Query: {
        users: async (_, obj) => Data.users,

        user: async (_, { id }) => {
            let user = Data.users.find((user) => user.id == id);
            if (!user) {
                throw new Error('unknown user');
            }
            return user;
        },

        login: async (_, { username, password }) => {
            let user = Data.users.find((user) => user.username === username && user.password === password);
            if (!user) {
                throw new Error('unknown user!');
            }

            const token = jwt.sign({ username: user.username, password: user.password, role: user.role }, 'mysecrete');
            return token;
        }
    }
};

export default resolvers;

El código anterior contiene resolutores para gestionar las consultas user, users y login.

Por último, debemos agregar una política de autenticación personalizada registrando el complemento Mercurius Auth. Para hacerlo, agregamos el siguiente código debajo del comentario register auth policy en el archivo index.js, dentro del directorio src. El comentario está en la línea 19:

app.register(mercuriusAuth, {
    authContext(context) {
        return { identity: context.reply.request.headers['x-user'] };
    },
    async applyPolicy(authDirectiveAST, parent, args, context, info) {
        const token = context.auth.identity;
        try {
            const claim = jwt.verify(token, 'mysecrete');
        } catch (error) {
            throw new Error(`An error occurred. Try again!`);
        }

        return true;
    },
    authDirective: 'auth'
});

En nuestra política personalizada anterior, el método authContext recupera el token del usuario en los encabezados, mientras que el método applyPolicy contiene las políticas personalizadas de autenticación y autorización.

Además, cuando falla la autenticación o autorización de un usuario, generamos un error con un mensaje genérico, como «Ocurrió un error. ¡Inténtalo de nuevo!». Este mensaje se muestra en lugar de un mensaje de error detallado del servidor, que podría exponer vulnerabilidades existentes.

Con esto, terminamos nuestro trabajo. En la siguiente sección, probaremos nuestras API.

Probar la API

Primero, iniciamos el servidor ejecutando npm run dev desde el directorio raíz. Luego, abrimos el entorno de GraphQL en http://localhost:4500/playground.

Ahora, cuando consultamos una API protegida, como users o user, obtenemos un error como el que se muestra a continuación:

Interfaz de GraphQL que muestra una consulta de usuarios que solicita detalles de usuario y una respuesta de error que dice: “Ocurrió un error. ¡Inténtalo de nuevo!”

Para que nuestra consulta se ejecute correctamente, debemos autenticarnos. Iniciemos sesión para obtener un token.

Para iniciar sesión, abre una pestaña nueva en el entorno de GraphQL y ejecuta la siguiente consulta:

query {
  login(username: "JohnDoe", password: "12345")
}

Si la consulta se ejecuta correctamente, se generará un token y se devolverá como se muestra en la siguiente imagen.

Interfaz de GraphQL que muestra una consulta de inicio de sesión con campos para el nombre de usuario y la contraseña, y un token de autenticación devuelto

Ten en cuenta que los datos del usuario usados para iniciar sesión ya están en el archivo index.js, dentro del directorio data.

Si intentas iniciar sesión con datos de usuario que no están en ese archivo (como el nombre de usuario «John1Doe»), se producirá un error, como se muestra a continuación:

Interfaz de GraphQL que muestra una consulta de inicio de sesión con campos de nombre de usuario y contraseña que devuelve un error de «¡usuario desconocido!»

Ahora, si incluimos nuestro token en los encabezados como x-user, podemos consultar correctamente nuestras API protegidas, como se muestra a continuación. Asegúrate de copiar el token recibido en la consulta de inicio de sesión y usarlo como valor de x-user.

Consulta users

Esta es una consulta users de ejemplo:

query {
  users {
    id
    username
    password
    email
    role
  }
}

Agrega tu token como valor del parámetro del encabezado HTTP x-user, como se muestra a continuación:

{
  "x-user": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG5Eb2UiLCJwYXNzd29yZCI6IjEyMzQ1Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjQ0NTA3MDE5fQ.faslGjI6x-ODO2LGYOaTHClGs2MXCBOoMlWPYnwoH18"
}

El resultado es el siguiente:

Interfaz de consultas GraphQL que muestra una consulta de usuarios, un encabezado de autorización HTTP y los datos de usuario devueltos, incluidos nombres de usuario, contraseñas, correos electrónicos y roles.

Consulta user

Esta es una consulta user de ejemplo:

query {
  user(id: "1") {
    id
    username
    email
    password
    role
  }
}

Agrega tu token como valor del parámetro del encabezado HTTP x-user, como se muestra a continuación:

{
  "x-user": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG5Eb2UiLCJwYXNzd29yZCI6IjEyMzQ1Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjQ0NTA3MDE5fQ.faslGjI6x-ODO2LGYOaTHClGs2MXCBOoMlWPYnwoH18"
}

El resultado es el siguiente:

Entorno de GraphQL que muestra una consulta de usuario, un encabezado de autorización HTTP y los datos devueltos del usuario, incluidos nombre de usuario, correo electrónico, contraseña y rol de administrador

Conclusión

En este artículo, descubrimos lo fácil que es proteger las API GraphQL con Fastify y GraphQL para crear una aplicación sencilla de Node.js.

Como vimos, GraphQL incluye algunas medidas de seguridad, como la validación y la verificación de tipos. Sin embargo, la flexibilidad y la capacidad que permiten a los usuarios solicitar datos a voluntad hacen que la seguridad siempre deba ser una prioridad.

En este artículo también revisamos algunas estrategias para proteger las API GraphQL: autenticación y autorización, límite de profundidad de las consultas, ocultación de errores y sanitización y validación de entradas.

Estas estrategias de seguridad son muy eficaces, pero podemos agregar más protección con otros métodos, como los tiempos de espera de las consultas y los límites de frecuencia, que especifican cuántas veces puede consultar un cliente una API en cada período.

Empieza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.

Prueba nuestra herramienta gratuita en línea de revisión de código JavaScript para ver cómo Snyk Code analiza tu código en busca de problemas de seguridad y calidad.