Creemos nuestra propia API de autenticación con Nodejs y GraphQL | Programar Plus

La autenticación es una de las tareas más desafiantes para los desarrolladores que recién comienzan con GraphQL. Hay muchas consideraciones técnicas, incluido qué ORM sería fácil de configurar, cómo generar tokens seguros y contraseñas hash, e incluso qué biblioteca HTTP usar y cómo usarla.

En este artículo, nos centraremos en autenticación local. Es quizás la forma más popular de manejar la autenticación en sitios web modernos y lo hace solicitando al usuario Email y contraseña (a diferencia de, digamos, usar la autenticación de Google).

Además, este artículo utiliza Apollo Server 2, JSON Web Tokens (JWT) y Sequelize ORM para crear una API de autenticación con Node.

Manejo de la autenticación

Como en un sistema de inicio de sesión:

  • Autenticación identifica o verifica a un usuario.
  • Autorización está validando las rutas (o partes de la aplicación) a las que el usuario autenticado puede tener acceso.

El flujo para implementar esto es:

  1. El usuario se registra mediante contraseña y correo electrónico.
  2. Las credenciales del usuario se almacenan en una base de datos.
  3. El usuario es redirigido al inicio de sesión cuando se completa el registro.
  4. El usuario tiene acceso a recursos específicos cuando se autentica.
  5. El estado del usuario se almacena en cualquiera de los medios de almacenamiento del navegador (p. Ej. localStorage, cookies, sesión) o JWT.

Prerrequisitos

Antes de sumergirnos en la implementación, aquí hay algunas cosas que deberá seguir.

  • Nodo 6 o superior
  • Hilo (recomendado) o NPM
  • Zona de juegos GraphQL
  • Conocimientos básicos de GraphQL y Node
  • … ¡Una mente inquisitiva!

Dependencias

Esta es una gran lista, así que entremos en ella:

  • Servidor Apollo: Un servidor GraphQL de código abierto que es compatible con cualquier tipo de cliente GraphQL. No usaremos Express para nuestro servidor en este proyecto. En su lugar, usaremos el poder de Apollo Server para exponer nuestra API GraphQL.
  • bcryptjs: Queremos aplicar un hash a las contraseñas de los usuarios en nuestra base de datos. Por eso usaremos bcrypt. Se basa en API de criptografía web‘s getRandomValues interfaz para obtener números aleatorios seguros.
  • dotenv: Usaremos dotenv para cargar variables de entorno desde nuestro .env Archivo.
  • jsonwebtoken: Una vez que el usuario haya iniciado sesión, cada solicitud posterior incluirá el JWT, lo que le permitirá acceder a rutas, servicios y recursos que están permitidos con ese token. jsonwebtokense utilizará para generar un JWT que se utilizará para autenticar a los usuarios.
  • Nodemon: Una herramienta que ayuda a desarrollar aplicaciones basadas en nodos al reiniciar automáticamente la aplicación de nodo cuando se detectan cambios en el directorio. No queremos cerrar e iniciar el servidor cada vez que hay un cambio en nuestro código. Nodemon inspecciona los cambios cada vez que se realizan en nuestra aplicación y reinicia automáticamente el servidor.
  • mysql2: Un cliente SQL para Node. Necesitamos que se conecte a nuestro servidor SQL para que podamos ejecutar migraciones.
  • secuelizar: Sequelize es un nodo ORM basado en promesas para Postgres, MySQL, MariaDB, SQLite y Microsoft SQL Server. Usaremos Sequelize para generar automáticamente nuestras migraciones y modelos.
  • secuela cli: Usaremos Sequelize CLI para ejecutar comandos de Sequelize. Instálelo globalmente con yarn add --global sequelize-cli en la terminal.

Configurar la estructura del directorio y el entorno de desarrollo

Creemos un nuevo proyecto. Crea una nueva carpeta y esto dentro de ella:

yarn init -y

El -y La bandera indica que estamos seleccionando sí a todos los yarn init preguntas y utilizando los valores predeterminados.

También deberíamos poner un package.json archivo en la carpeta, así que instalemos las dependencias del proyecto:

yarn add apollo-server bcrpytjs dotenv jsonwebtoken nodemon sequelize sqlite3

A continuación, agreguemos Babeto nuestro entorno de desarrollo:

yarn add babel-cli babel-preset-env babel-preset-stage-0 --dev

Ahora, configuremos Babel. Ejecutar toque .babelrc en la terminal. Eso crea y abre un archivo de configuración de Babel y, en él, agregaremos esto:

{
  "presets": ["env", "stage-0"]
}

También sería bueno si nuestro servidor se inicia y también migra datos. Podemos automatizar eso actualizando package.json con este:

"scripts": {
  "migrate": " sequelize db:migrate",
  "dev": "nodemon src/server --exec babel-node -e js",
  "start": "node src/server",
  "test": "echo "Error: no test specified" && exit 1"
},

Aquí está nuestro package.json archivo en su totalidad en este punto:

{
  "name": "graphql-auth",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "migrate": " sequelize db:migrate",
    "dev": "nodemon src/server --exec babel-node -e js",
    "start": "node src/server",
    "test": "echo "Error: no test specified" && exit 1"
  },
  "dependencies": {
    "apollo-server": "^2.17.0",
    "bcryptjs": "^2.4.3",
    "dotenv": "^8.2.0",
    "jsonwebtoken": "^8.5.1",
    "nodemon": "^2.0.4",
    "sequelize": "^6.3.5",
    "sqlite3": "^5.0.0"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-stage-0": "^6.24.1"
  }
}

Ahora que nuestro entorno de desarrollo está configurado, pasemos a la base de datos donde almacenaremos las cosas.

Configuración de la base de datos

Usaremos MySQL como nuestra base de datos y Sequelize ORM para nuestras relaciones. Ejecute sequelize init (asumiendo que lo instaló globalmente antes). El comando debería crear tres carpetas: /config /models y /migrations. En este punto, nuestra estructura de directorio de proyectos está tomando forma.

Configuremos nuestra base de datos. Primero, crea un .env archivo en el directorio raíz del proyecto y pegue esto:

NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=
DB_PASSWORD=
DB_NAME=

Entonces ve al /config carpeta que acabamos de crear y renombrar la config.json archivar allí para config.js. Luego, suelte este código allí:

require('dotenv').config()
const dbDetails = {
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  host: process.env.DB_HOST,
  dialect: 'mysql'
}
module.exports = {
  development: dbDetails,
  production: dbDetails
}

Aquí estamos leyendo los detalles de la base de datos que configuramos en nuestro .env Archivo. process.env es una variable global inyectada por Node y se usa para representar el estado actual del entorno del sistema.

Actualicemos los detalles de nuestra base de datos con los datos adecuados. Abra la base de datos SQL y cree una tabla llamada graphql_auth. Yo uso Laragon como mi servidor local y phpmyadmin para gestionar las tablas de la base de datos.

Sea lo que sea que use, queremos actualizar el .env archivo con la información más reciente:

NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=graphql_auth
DB_PASSWORD=
DB_NAME=<your_db_username_here>

Configuremos Sequelize. Crear un .sequelizerc archivo en la raíz del proyecto y pegue esto:

const path = require('path')


module.exports = {
  config: path.resolve('config', 'config.js')
}

Ahora integremos nuestra configuración en los modelos. Ve a la index.js en el /models carpeta y edite el config variable.

const config = require(__dirname + '/../../config/config.js')[env]

Finalmente, escribamos nuestro modelo. Para este proyecto, necesitamos un User modelo. Usemos Sequelize para generar automáticamente el modelo. Esto es lo que necesitamos ejecutar en la terminal para configurarlo:

sequelize model:generate --name User --attributes username:string,email:string,password:string

Editemos el modelo que crea para nosotros. Ir user.js en el /models carpeta y pegue esto:

'use strict';
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    username: {
      type: DataTypes.STRING,
    },
    email: {
      type: DataTypes.STRING,  
    },
    password: {
      type: DataTypes.STRING,
    }
  }, {});
  return User;
};

Aquí, creamos atributos y campos para nombre de usuario, correo electrónico y contraseña. Ejecutemos una migración para realizar un seguimiento de los cambios en nuestro esquema:

yarn migrate

Escribamos ahora el esquema y los resolutores.

Integrar esquemas y resolutores con el servidor GraphQL

En esta sección, definiremos nuestro esquema, escribiremos funciones de resolución y las expondremos en nuestro servidor.

El esquema

En la carpeta src, cree una nueva carpeta llamada /schema y crea un archivo llamado schema.js. Pega el siguiente código:

const { gql } = require('apollo-server')
const typeDefs = gql`
  type User {
    id: Int!
    username: String
    email: String!
  }
  type AuthPayload {
    token: String!
    user: User!
  }
  type Query {
    user(id: Int!): User
    allUsers: [User!]!
    me: User
  }
  type Mutation {
    registerUser(username: String, email: String!, password: String!): AuthPayload!
    login (email: String!, password: String!): AuthPayload!
  }
`
module.exports = typeDefs

Aquí hemos importado graphql-tag de apollo-server. Apollo Server requiere envolver nuestro esquema con gql.

Los resolutores

En el src carpeta, cree una nueva carpeta llamada /resolvers y crea un archivo en él llamado resolver.js. Pega el siguiente código:

const bcrypt = require('bcryptjs')
const jsonwebtoken = require('jsonwebtoken')
const models = require('../models')
require('dotenv').config()
const resolvers = {
    Query: {
      async me(_, args, { user }) {
        if(!user) throw new Error('You are not authenticated')
        return await models.User.findByPk(user.id)
      },
      async user(root, { id }, { user }) {
        try {
          if(!user) throw new Error('You are not authenticated!')
          return models.User.findByPk(id)
        } catch (error) {
          throw new Error(error.message)
        }
      },
      async allUsers(root, args, { user }) {
        try {
          if (!user) throw new Error('You are not authenticated!')
          return models.User.findAll()
        } catch (error) {
          throw new Error(error.message)
        }
      }
    },
    Mutation: {
      async registerUser(root, { username, email, password }) {
        try {
          const user = await models.User.create({
            username,
            email,
            password: await bcrypt.hash(password, 10)
          })
          const token = jsonwebtoken.sign(
            { id: user.id, email: user.email},
            process.env.JWT_SECRET,
            { expiresIn: '1y' }
          )
          return {
            token, id: user.id, username: user.username, email: user.email, message: "Authentication succesfull"
          }
        } catch (error) {
          throw new Error(error.message)
        }
      },
      async login(_, { email, password }) {
        try {
          const user = await models.User.findOne({ where: { email }})
          if (!user) {
            throw new Error('No user with that email')
          }
          const isValid = await bcrypt.compare(password, user.password)
          if (!isValid) {
            throw new Error('Incorrect password')
          }
          // return jwt
          const token = jsonwebtoken.sign(
            { id: user.id, email: user.email},
            process.env.JWT_SECRET,
            { expiresIn: '1d'}
          )
          return {
           token, user
          }
      } catch (error) {
        throw new Error(error.message)
      }
    }
  },


}
module.exports = resolvers

Eso es mucho código, así que veamos qué está sucediendo allí.

Primero importamos nuestros modelos, bcrypt y  jsonwebtoken, y luego inicializamos nuestras variables ambientales.

A continuación, están las funciones de resolución. En el solucionador de consultas, tenemos tres funciones (me, user y allUsers):

  • me consulta obtiene los detalles de la loggedIn usuario. Acepta un user objeto como argumento de contexto. El contexto se utiliza para proporcionar acceso a nuestra base de datos que se utiliza para cargar los datos de un usuario mediante la identificación proporcionada como un argumento en la consulta.
  • user consulta obtiene los detalles de un usuario en función de su ID. Acepta id como el argumento de contexto y un user objeto.
  • alluser consulta devuelve los detalles de todos los usuarios.

user sería un objeto si el estado del usuario es loggedIn y seria null, si el usuario no lo es. Creamos este usuario en nuestras mutaciones.

En el resolutor de mutaciones, tenemos dos funciones (registerUser y loginUser):

  • registerUser acepta el username, email y password de El user y crea una nueva fila con estos campos en nuestra base de datos. Es importante tener en cuenta que usamos el paquete bcryptjs para codificar la contraseña de los usuarios con bcrypt.hash(password, 10). jsonwebtoken.sign firma sincrónicamente la carga útil dada en una cadena JSON Web Token (en este caso, el usuario id y email). Por fin, registerUser devuelve la cadena JWT y el perfil de usuario si tiene éxito y devuelve un mensaje de error si algo sale mal.
  • login acepta email y password y comprueba si estos detalles coinciden con el que se proporcionó. Primero, verificamos si el email El valor ya existe en algún lugar de la base de datos del usuario.
models.User.findOne({ where: { email }})
if (!user) {
  throw new Error('No user with that email')
}

Entonces, usamos bcrypt’s bcrypt.compare método para comprobar si la contraseña coincide.

const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
  throw new Error('Incorrect password')
}

Entonces, al igual que hicimos anteriormente en registerUser, usamos jsonwebtoken.sign para generar una cadena JWT. El login mutación devuelve el token y user objeto.

Ahora agreguemos el JWT_SECRET para nuestro .env Archivo.

JWT_SECRET=somereallylongsecret

El servidor

¡Finalmente, el servidor! Crear un server.js en la carpeta raíz del proyecto y pega esto:

const { ApolloServer } = require('apollo-server')
const jwt =  require('jsonwebtoken')
const typeDefs = require('./schema/schema')
const resolvers = require('./resolvers/resolvers')
require('dotenv').config()
const { JWT_SECRET, PORT } = process.env
const getUser = token => {
  try {
    if (token) {
      return jwt.verify(token, JWT_SECRET)
    }
    return null
  } catch (error) {
    return null
  }
}
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    const token = req.get('Authorization') || ''
    return { user: getUser(token.replace('Bearer', ''))}
  },
  introspection: true,
  playground: true
})
server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

Aquí, importamos el esquema, los resolutores y jwt, e inicializamos nuestras variables de entorno. Primero, verificamos el token JWT con verify. jwt.verify acepta el token y el secreto JWT como parámetros.

A continuación, creamos nuestro servidor con un ApolloServer instancia que acepta typeDefs y resolutores.

¡Tenemos un servidor! Empecemos corriendo yarn dev en la terminal.

Probando la API

Probemos ahora la API GraphQL con GraphQL Playground. Deberíamos poder registrarnos, iniciar sesión y ver todos los usuarios, incluido un solo usuario, por ID.

Comenzaremos abriendo la aplicación GraphQL Playground o simplemente abriremos localhost://4000 en el navegador para acceder a él.

Mutación para registro de usuario

mutation {
  registerUser(username: "Wizzy", email: "[email protected]", password: "wizzyekpot" ){
    token
  }
}

Deberíamos obtener algo como esto:

{
  "data": {
    "registerUser": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzAwLCJleHAiOjE2MzA3OTc5MDB9.gmeynGR9Zwng8cIJR75Qrob9bovnRQT242n6vfBt5PY"
    }
  }
}

Mutación para iniciar sesión

Iniciemos sesión con los detalles del usuario que acabamos de crear:

mutation {
  login(email:"[email protected]" password:"wizzyekpot"){
    token
  }
}

Deberíamos obtener algo como esto:

{
  "data": {
    "login": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
    }
  }
}

¡Impresionante!

Consulta para un solo usuario

Para que podamos consultar a un solo usuario, debemos pasar el token de usuario como encabezado de autorización. Vaya a la pestaña Encabezados HTTP.

Mostrando la interfaz GraphQL con la pestaña HTTP Headers resaltada en rojo en la esquina inferior izquierda de la pantalla,

… y pega esto:

{
  "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsImVtYWlsIjoiZWtwb3RAZ21haWwuY29tIiwiaWF0IjoxNTk5MjQwMzcwLCJleHAiOjE1OTkzMjY3NzB9.PDiBKyq58nWxlgTOQYzbtKJ-HkzxemVppLA5nBdm4nc"
}

Aquí está la consulta:

query myself{
  me {
    id
    email
    username
  }
}

Y deberíamos obtener algo como esto:

{
  "data": {
    "me": {
      "id": 15,
      "email": "[email protected]",
      "username": "Wizzy"
    }
  }
}

¡Estupendo! Ahora obtengamos un usuario por ID:

query singleUser{
  user(id:15){
    id
    email
    username
  }
}

Y aquí está la consulta para obtener todos los usuarios:

{
  allUsers{
    id
    username
    email
  }
}

Resumen

La autenticación es una de las tareas más difíciles cuando se trata de crear sitios web que la requieran. GraphQL nos permitió crear una API de autenticación completa con un solo punto final. Sequelize ORM hace que la creación de relaciones con nuestra base de datos SQL sea tan fácil que apenas tuvimos que preocuparnos por nuestros modelos. También es notable que no necesitáramos una biblioteca de servidor HTTP (como Express) y usamos Apollo GraphQL como middleware. Apollo Server 2, ahora nos permite crear nuestros propios servidores GraphQL independientes de la biblioteca.

Consulte el código fuente de este tutorial en GitHub.

(Visited 4 times, 1 visits today)