Catégories
Astuces et Design

Créons notre propre API d'authentification avec Nodejs et GraphQL

L'authentification est l'une des tâches les plus difficiles pour les développeurs qui commencent tout juste avec GraphQL. Il y a beaucoup de considérations techniques, notamment ce que l'ORM serait facile à configurer, comment générer des jetons sécurisés et des mots de passe de hachage, et même quelle bibliothèque HTTP utiliser et comment l'utiliser.

Dans cet article, nous allons nous concentrer sur authentification locale. C'est peut-être le moyen le plus populaire de gérer l'authentification sur les sites Web modernes et le fait en demandant à l'utilisateur email et mot de passe (par opposition à, par exemple, utiliser l'authentification Google.)

De plus, cet article utilise Apollo Server 2, JSON Web Tokens (JWT) et Sequelize ORM pour créer une API d'authentification avec Node.

Gestion de l'authentification

Comme dans, un système de connexion:

  • Authentification identifie ou vérifie un utilisateur.
  • Autorisation valide les routes (ou parties de l'application) auxquelles l'utilisateur authentifié peut avoir accès.

Le flux de mise en œuvre est le suivant:

  1. L'utilisateur s'inscrit en utilisant le mot de passe et l'email
  2. Les informations d'identification de l'utilisateur sont stockées dans une base de données
  3. L'utilisateur est redirigé vers la connexion lorsque l'enregistrement est terminé
  4. L'utilisateur a accès à des ressources spécifiques lorsqu'il est authentifié
  5. L'état de l'utilisateur est stocké sur l'un des supports de stockage du navigateur (par ex. localStorage, cookies, session) ou JWT.

Conditions préalables

Avant de plonger dans la mise en œuvre, voici quelques éléments que vous devrez suivre.

Dépendances

C'est une longue liste, alors allons-y:

  • Serveur Apollo: Un serveur GraphQL open-source compatible avec tout type de client GraphQL. Nous n'utiliserons pas Express pour notre serveur dans ce projet. Au lieu de cela, nous utiliserons la puissance d'Apollo Server pour exposer notre API GraphQL.
  • bcryptjs: Nous voulons hacher les mots de passe des utilisateurs dans notre base de données. C’est pourquoi nous utiliserons bcrypt. Il repose sur API Web Crypto«S getRandomValues interface pour obtenir des nombres aléatoires sécurisés.
  • dotenv: Nous utiliserons dotenv pour charger les variables d'environnement depuis notre .env fichier.
  • jsonwebtoken: Une fois que l'utilisateur est connecté, chaque demande suivante inclura le JWT, permettant à l'utilisateur d'accéder aux routes, services et ressources qui sont autorisés avec ce jeton. jsonwebtokensera utilisé pour générer un JWT qui sera utilisé pour authentifier les utilisateurs.
  • nodemon: Outil qui permet de développer des applications basées sur les nœuds en redémarrant automatiquement l'application de nœuds lorsque des modifications dans le répertoire sont détectées. Nous ne voulons pas fermer et démarrer le serveur chaque fois qu’il y a un changement dans notre code. Nodemon inspecte les modifications à chaque fois dans notre application et redémarre automatiquement le serveur.
  • mysql2: Un client SQL pour Node. Nous avons besoin qu'il se connecte à notre serveur SQL pour pouvoir exécuter des migrations.
  • séquestrer: Sequelize est un ORM Node basé sur la promesse pour Postgres, MySQL, MariaDB, SQLite et Microsoft SQL Server. Nous utiliserons Sequelize pour générer automatiquement nos migrations et modèles.
  • séquencer cli: Nous utiliserons Sequelize CLI pour exécuter les commandes Sequelize. Installez-le globalement avec yarn add --global sequelize-cli dans le terminal.

Structure du répertoire d'installation et environnement de développement

Créons un tout nouveau projet. Créez un nouveau dossier et ceci à l'intérieur de celui-ci:

yarn init -y

le -y l'indicateur indique que nous sélectionnons oui à tous les yarn init questions et en utilisant les valeurs par défaut.

Nous devrions également mettre un package.json fichier dans le dossier, alors installons les dépendances du projet:

yarn add apollo-server bcrpytjs dotenv jsonwebtoken nodemon sequelize sqlite3

Ensuite, ajoutons Babeto à notre environnement de développement:

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

Maintenant, configurons Babel. Run touch .babelrc dans le terminal. Cela crée et ouvre un fichier de configuration Babel et, dans celui-ci, nous ajouterons ceci:

{
  "presets": ("env", "stage-0")
}

Ce serait également bien si notre serveur démarre et migre également les données. Nous pouvons automatiser cela en mettant à jour package.json avec ça:

"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"
},

Voici notre package.json fichier dans son intégralité à ce stade:

{
  "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"
  }
}

Maintenant que notre environnement de développement est configuré, passons à la base de données dans laquelle nous allons stocker les éléments.

Configuration de la base de données

Nous utiliserons MySQL comme base de données et Sequelize ORM pour nos relations. Exécutez sequelize init (en supposant que vous l'aviez installé globalement plus tôt). La commande doit créer trois dossiers: /config /models et /migrations. À ce stade, la structure de nos répertoires de projet se forme.

Configurons notre base de données. Commencez par créer un .env fichier dans le répertoire racine du projet et collez ceci:

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

Ensuite, allez à la /config dossier que nous venons de créer et renommer le config.json déposer là-dedans pour config.js. Ensuite, déposez ce code là-dedans:

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
}

Ici, nous lisons les détails de la base de données que nous définissons dans notre .env fichier. process.env est une variable globale injectée par Node et utilisée pour représenter l'état actuel de l'environnement système.

Mettons à jour les détails de notre base de données avec les données appropriées. Ouvrez la base de données SQL et créez une table appelée graphql_auth. J'utilise Laragon comme serveur local et phpmyadmin pour gérer les tables de la base de données.

Quoi que vous utilisiez, nous voudrons mettre à jour le .env fichier avec les dernières informations:

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

Configurons Sequelize. Créer un .sequelizerc à la racine du projet et collez-le:

const path = require('path')


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

Maintenant, intégrons notre configuration dans les modèles. Aller au index.js dans le /models dossier et modifiez le config variable.

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

Enfin, écrivons notre modèle. Pour ce projet, nous avons besoin d'un User modèle. Utilisons Sequelize pour générer automatiquement le modèle. Voici ce que nous devons exécuter dans le terminal pour configurer cela:

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

Modifions le modèle qui crée pour nous. Aller à user.js dans le /models dossier et collez ceci:

'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;
};

Ici, nous avons créé des attributs et des champs pour le nom d'utilisateur, l'email et le mot de passe. Lançons une migration pour suivre les modifications de notre schéma:

yarn migrate

Écrivons maintenant le schéma et les résolveurs.

Intégrez le schéma et les résolveurs au serveur GraphQL

Dans cette section, nous allons définir notre schéma, écrire des fonctions de résolution et les exposer sur notre serveur.

Le schéma

Dans le dossier src, créez un nouveau dossier appelé /schema et créez un fichier appelé schema.js. Collez le code suivant:

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

Ici, nous avons importé graphql-tag depuis apollo-server. Apollo Server nécessite d'encapsuler notre schéma avec gql.

Les résolveurs

dans le src dossier, créez un nouveau dossier appelé /resolvers et créez-y un fichier appelé resolver.js. Collez le code suivant:

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

C'est beaucoup de code, alors voyons ce qui se passe là-dedans.

Nous avons d'abord importé nos modèles, bcrypt et  jsonwebtoken, puis initialisé nos variables environnementales.

Viennent ensuite les fonctions du résolveur. Dans le résolveur de requêtes, nous avons trois fonctions (me, user et allUsers):

  • me la requête récupère les détails de la loggedIn utilisateur. Il accepte un user object comme argument de contexte. le le contexte est utilisé pour fournir l'accès à notre base de données qui est utilisée pour charger les données d'un utilisateur par l'ID fourni en tant que argument dans la requête.
  • user La requête récupère les détails d'un utilisateur en fonction de son ID. Il accepte id comme argument de contexte et un user objet.
  • alluser query renvoie les détails de tous les utilisateurs.

user serait un objet si l'état de l'utilisateur est loggedIn et ce serait null, si l'utilisateur ne l'est pas. Nous créerions cet utilisateur dans nos mutations.

Dans le résolveur de mutation, nous avons deux fonctions (registerUser et loginUser):

  • registerUser accepte le username, email et password du user et crée une nouvelle ligne avec ces champs dans notre base de données. Il est important de noter que nous avons utilisé le package bcryptjs pour hacher le mot de passe des utilisateurs avec bcrypt.hash(password, 10). jsonwebtoken.sign signe de manière synchrone la charge utile donnée dans une chaîne de jeton Web JSON (dans ce cas, l'utilisateur id et email). Finalement, registerUser renvoie la chaîne JWT et le profil utilisateur en cas de succès et renvoie un message d'erreur en cas de problème.
  • login accepte email et password , et vérifie si ces détails correspondent à ceux qui ont été fournis. Tout d'abord, nous vérifions si le email la valeur existe déjà quelque part dans la base de données utilisateur.
models.User.findOne({ where: { email }})
if (!user) {
  throw new Error('No user with that email')
}

Ensuite, nous utilisons bcrypt bcrypt.compare méthode pour vérifier si le mot de passe correspond.

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

Ensuite, tout comme nous l'avons fait précédemment dans registerUser, nous utilisons jsonwebtoken.sign pour générer une chaîne JWT. le login la mutation renvoie le jeton et user objet.

Ajoutons maintenant le JWT_SECRET à notre .env fichier.

JWT_SECRET=somereallylongsecret

Le serveur

Enfin, le serveur! Créer un server.js dans le dossier racine du projet et collez ceci:

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}`);
});

Ici, nous importons le schéma, les résolveurs et jwt, et initialisons nos variables d'environnement. Tout d'abord, nous vérifions le jeton JWT avec verify. jwt.verify accepte le jeton et le secret JWT comme paramètres.

Ensuite, nous créons notre serveur avec un ApolloServer instance qui accepte typeDefs et résolveurs.

Nous avons un serveur! Commençons par courir yarn dev dans le terminal.

Tester l'API

Testons maintenant l'API GraphQL avec GraphQL Playground. Nous devrions pouvoir enregistrer, ouvrir une session et afficher tous les utilisateurs – y compris un seul utilisateur – par ID.

Nous allons commencer par ouvrir l'application GraphQL Playground ou simplement ouvrir localhost://4000 dans le navigateur pour y accéder.

Mutation pour l'utilisateur du registre

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

Nous devrions obtenir quelque chose comme ceci:

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

Mutation pour la connexion

Connectez-vous maintenant avec les informations utilisateur que nous venons de créer:

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

Nous devrions obtenir quelque chose comme ceci:

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

Impressionnant!

Requête pour un seul utilisateur

Pour que nous interrogions un seul utilisateur, nous devons transmettre le jeton d'utilisateur en tant qu'en-tête d'autorisation. Accédez à l'onglet En-têtes HTTP.

Affichage de l'interface GraphQL avec l'onglet En-têtes HTTP surligné en rouge dans le coin inférieur gauche de l'écran,

… Et collez ceci:

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

Voici la requête:

query myself{
  me {
    id
    email
    username
  }
}

Et nous devrions obtenir quelque chose comme ça:

{
  "data": {
    "me": {
      "id": 15,
      "email": "(email protected)",
      "username": "Wizzy"
    }
  }
}

Génial! Obtenons maintenant un utilisateur par ID:

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

Et voici la requête pour obtenir tous les utilisateurs:

{
  allUsers{
    id
    username
    email
  }
}

Résumé

L'authentification est l'une des tâches les plus difficiles lorsqu'il s'agit de créer des sites Web qui en ont besoin. GraphQL nous a permis de créer une API d'authentification complète avec un seul point de terminaison. Sequelize ORM rend la création de relations avec notre base de données SQL si facile que nous avons à peine eu à nous soucier de nos modèles. Il est également remarquable que nous n’ayons pas besoin d’une bibliothèque de serveur HTTP (comme Express) et que nous utilisions Apollo GraphQL comme middleware. Apollo Server 2 nous permet désormais de créer nos propres serveurs GraphQL indépendants de la bibliothèque!

Consultez le code source de ce tutoriel sur GitHub.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *