Fermer

août 24, 2020

Une introduction à GraphQL: l'authentification


La spécification GraphQL qui définit un système de type, une requête et un langage de schéma pour votre API Web, et un algorithme d'exécution indiquant comment un service (ou moteur) GraphQL doit valider et exécuter des requêtes par rapport au schéma GraphQL. Dans cet article, vous apprendrez à implémenter l'authentification dans un serveur GraphQL.

GraphQL, décrit comme un langage de requête et de manipulation de données pour les API, et un environnement d'exécution pour répondre aux requêtes avec des données existantes, permet à différents clients d'utiliser votre API et de rechercher uniquement les données dont ils ont besoin. Cela permet de résoudre certains problèmes de performances rencontrés par certains services REST – surextraction et sous-extraction . La spécification GraphQL définit un système de types, un langage de requête et un langage de schéma pour votre API Web, ainsi qu'un algorithme d'exécution pour la manière dont un service (ou moteur) GraphQL doit valider et exécuter des requêtes sur le schéma GraphQL.

Il existe différentes manières de gérer l'authentification dans un serveur GraphQL, et dans cet article, je vais vous expliquer comment créer des résolveurs d'inscription et de connexion, puis créer une fonction wrapper qui sera utilisée pour envelopper des résolveurs spécifiques pour les champs racine nous voulons être accessibles uniquement aux utilisateurs authentifiés.

Nous travaillerons avec un serveur GraphQL existant en y ajoutant de nouveaux résolveurs et en protégeant les résolveurs existants. Si vous avez suivi les articles précédents avant celui-ci, vous devriez être familier avec le projet et probablement déjà avoir le code d'où nous nous sommes arrêtés dans le dernier article, An Introduction to GraphQL: Subscriptions .

If vous ne possédez pas déjà ce projet, mais souhaitez coder en même temps, téléchargez le projet depuis GitHub et copiez les fichiers du dossier src-part-3 dans le dossier principal dossier src . Suivez ensuite les instructions du fichier README pour configurer le projet.

Autoriser l'inscription et la connexion

Nous ajouterons deux nouvelles opérations au schéma: une pour que les utilisateurs s'inscrivent, et une autre pour la connexion. Nous stockerons les informations utilisateur dans la base de données; par conséquent, nous devons mettre à jour le modèle de base de données. Ouvrez le fichier src / prisma / datamodel.prisma et ajoutez-y le modèle ci-dessous.

 tapez User {
  J'ai fait! @id
  nom: String!
  email: String! @unique
  mot de passe: String!
}

Le modèle User représente l'utilisateur qui doit être authentifié pour utiliser l'API, et nous stockerons ces informations dans la base de données. Après avoir mis à jour le modèle de données, nous devons mettre à jour le serveur Prisma avec ce changement. Ouvrez le terminal et accédez au répertoire src / prisma et exécutez primsa deploy .

 Prisma deploy - GraphQL

Lorsque cela est terminé avec succès, exécutez la commande prisma generate pour mettre à jour le client prisma généré automatiquement.

Mettre à jour le schéma GraphQL

Une fois notre modèle de données mis à jour, nous allons maintenant mettre à jour le schéma GraphQL avec deux nouveaux champs racine sur le type Mutation. Ouvrez src / index.js et ajoutez deux nouveaux champs racine, signup et signin au type Mutation .

 inscription (email: String !, mot de passe: String !, nom: String!): AuthPayload
signin (email: String !, mot de passe: String!): AuthPayload

Ces mutations seront utilisées pour les demandes d'inscription et de connexion et renverront des données de type AuthPayload . Allez-y et ajoutez la définition de ce nouveau type au schéma:

 type AuthPayload {
  jeton: String!
  utilisateur: Utilisateur!
}

type User {
  J'ai fait!
  nom: String!
  email: String!
}

Avec ces nouvelles modifications, votre définition de schéma devrait correspondre à ce que vous voyez ci-dessous:

 const typeDefs = `
type Livre {
    J'ai fait!
    titre: String!
    pages: Int
    chapitres: Int
    auteurs: [Author!]!
}

type Auteur {
    J'ai fait!
    nom: String!
    livres: [Book!]!
}

type Requête {
  livres: [Book!]
  book (id: ID!): Réserver
  auteurs: [Author!]
}

type Mutation {
  livre (titre: String !, auteurs: [String!] !, pages: Int, chapitres: Int): Book!
  inscription (email: String !, mot de passe: String !, nom: String!): AuthPayload
  signin (email: String !, mot de passe: String!): AuthPayload
}

type Abonnement {
  newBook ​​(containsTitle: String): Livre!
}

type AuthPayload {
  jeton: String!
  utilisateur: Utilisateur!
}

type User {
  J'ai fait!
  nom: String!
  email: String!
}
`;

Implémentation des résolveurs

Maintenant que nous avons ajouté de nouveaux types et étendu le type Mutation nous devons implémenter des fonctions de résolveur pour eux. Ouvrez src / index.js allez à la ligne 82 où nous pouvons ajouter des fonctions de résolveur pour les champs racine de mutation et collez le code ci-dessous:

 signup: async (root, args, context, info) => {
  mot de passe const = attendre bcrypt.hash (args.password, 10);
  const user = attendre context.prisma.createUser ({... args, mot de passe});
  jeton const = jwt.sign ({userId: user.id}, APP_SECRET);

  revenir {
    jeton,
    utilisateur
  };
},
signin: async (racine, arguments, contexte, info) => {
  const user = attendre context.prisma.user ({email: args.email});
  if (! utilisateur) {
    throw new Error ("No such user found");
  }
  const valide = attendre bcrypt.compare (args.password, user.password);
  if (! valide) {
    throw new Error ("Mot de passe invalide");
  }

  jeton const = jwt.sign ({userId: user.id}, APP_SECRET);

  revenir {
    jeton,
    utilisateur
  };
}

Le code que vous venez d'ajouter gérera l'inscription et la connexion à l'application. Nous avons utilisé deux bibliothèques bcryptjs et jsonwebtoken (que nous ajouterons plus tard) pour crypter le mot de passe et gérer la création et la validation des jetons. Dans le résolveur signup le mot de passe est haché avant d'enregistrer les données utilisateur dans la base de données. Ensuite, nous utilisons la bibliothèque jsonwebtoken pour générer un jeton Web JSON en appelant jwt.sign () avec le secret d'application utilisé pour signer le jeton. Nous ajouterons le APP_SECRET plus tard. Le résolveur de connexion valide l'e-mail et le mot de passe. S'il est correct, il signe un jeton et renvoie un objet qui correspond au type AuthPayLoad qui est le type de retour pour les mutations signup et signin . [19659004] Je tiens à souligner que j'ai volontairement ignoré l'ajout d'un délai d'expiration au jeton généré. Cela signifie que le jeton obtenu par un client sera utilisé à tout moment pour accéder à l'API. Dans une application de production, je vous conseille d'ajouter une période d'expiration pour le jeton et de la valider sur le serveur.

Pendant que index.js est ouvert, ajoutez l'instruction de code ci-dessous après la ligne 2:

 const bcrypt = require ("bcryptjs");
const jwt = require ("jsonwebtoken");
const APP_SECRET = "GraphQL-Vue-React";

Ouvrez maintenant la ligne de commande et exécutez la commande ci-dessous pour installer les dépendances nécessaires.

 npm install --save jsonwebtoken bcryptjs

Exigence d'une authentification pour l'API

Jusqu'à présent, nous avons mis en œuvre un mécanisme permettant aux utilisateurs de se connecter et d'obtenir un jeton qui sera utilisé pour les valider en tant qu'utilisateur. Nous allons maintenant passer à une nouvelle exigence pour l'API. Ce qui est:

Seuls les utilisateurs authentifiés doivent appeler l'opération de mutation de livre.

Nous allons l'implémenter en validant le jeton de la requête. Nous utiliserons un jeton de connexion dans un en-tête d'autorisation HTTP. Une fois validé, nous vérifions que l'ID utilisateur du jeton correspond à un utilisateur valide dans la base de données. S'il est valide, nous plaçons l'objet utilisateur dans l'argument context que les fonctions du résolveur recevront.

Commençons par mettre l'objet utilisateur dans le contexte. Ouvrez src / index.js et allez à la ligne 129 où le serveur GraphQL est en cours d'initialisation. Mettez à jour le champ context comme suit:

 context: async ({request}) => {
  laissez l'utilisateur;
  laissez isAuthenticated = false;
  // récupère le jeton utilisateur des en-têtes
  autorisation const = request.get ("Autorisation");
  if (autorisation) {
    jeton const = autorisation.replace ("Porteur", "");
    // essaie de récupérer un utilisateur avec le jeton
    user = attendre getUser (jeton);
    if (utilisateur) isAuthenticated = true;
  }

  // ajoute l'utilisateur et le client prisma au contexte
  return {isAuthenticated, user, prisma};
};

Auparavant, nous avons mappé un objet incluant le client prisma au contexte . Cette fois-ci, nous lui donnons une fonction et cette fonction sera utilisée pour construire l'objet de contexte que chaque fonction de résolveur reçoit. Dans cette fonction, nous obtenons le jeton de l'en-tête de la requête et le passons à la fonction getUser () . Une fois résolu, nous renvoyons un objet qui inclut le client prisma, l'objet utilisateur et un champ supplémentaire permettant de vérifier si la requête est authentifiée.

Nous allons définir la fonction getUser qui était utilisé précédemment dans index.js pour avoir la signature ci-dessous:

 fonction async getUser (token) {
  const {userId} = jwt.verify (jeton, APP_SECRET);
  return wait prisma.user ({id: userId});
}

Notre prochaine étape sera de définir une fonction wrapper qui sera utilisée pour envelopper les résolveurs que nous voulons être authentifiés. Cette fonction utilisera les informations de l'objet de contexte pour déterminer l'accès à un résolveur. Ajoutez cette nouvelle fonction dans src / index.js .

 function authenticate (resolver) {
  fonction de retour (racine, arguments, contexte, info) {
    if (context.isAuthenticated) {
      return resolver (racine, arguments, contexte, info);
    }
    throw new Error (`Accès refusé!`);
  };
}

Ce que cette fonction vérifie, c'est si l'utilisateur est authentifié. S'ils sont authentifiés, il appellera la fonction de résolution qui lui a été transmise. Si ce n'est pas le cas, une exception sera levée.

Allez maintenant à la fonction de résolution du livre et enveloppez-le avec la fonction authenticate .

 book: authenticate ( async (racine, arguments, contexte, info) => {
      laissez auteursToCreate = [];
      laissez auteursToConnect = [];

      for (const authorName of args.authors) {
        const author = attendre context.prisma.author ({nom: authorName});
        if (auteur) auteursToConnect.push (auteur);
        else auteursToCreate.push ({name: authorName});
      }

      retourne context.prisma.createBook ({
        titre: args.title,
        pages: args.pages,
        chapitres: args.chapters,
        auteurs: {
          create: auteursToCreate,
          connecter: auteursToConnect
        }
      });
    }),

Test de l'application

Maintenant, nous sommes prêts à tester le flux d'authentification que nous avons ajouté à l'API. Allez-y et ouvrez la ligne de commande dans le répertoire racine de votre projet. Exécutez node src / index.js pour démarrer le serveur et accédez à http: // localhost: 4000 dans le navigateur.

Exécutez la requête suivante pour créer un nouveau livre:

 mutation {
  livre (titre: "GRAND Stack", auteurs: ["James Blunt"]) {
    Titre
  }
}

Vous devriez recevoir le message d'erreur Accès refusé! comme réponse. Nous avons besoin d'un jeton pour pouvoir exécuter cette opération. Nous allons inscrire un nouvel utilisateur et utiliser le jeton renvoyé dans l'en-tête d'autorisation.

Exécutez la requête suivante pour créer un nouvel utilisateur:

 mutation {
  inscription (e-mail: "test@test.com", nom: "Compte test", mot de passe: "test") {
    jeton
  }
}

Il exécutera la mutation et retournera un jeton. Ouvrez le volet HTTP HEADERS dans le coin inférieur gauche de l'aire de jeu et spécifiez l'en-tête Authorization comme suit:

 {
  "Autorisation": "Porteur __TOKEN__"
}

Remplacez __ TOKEN __ par le jeton dans la réponse que vous avez obtenue de la dernière requête de mutation. Maintenant, réexécutez la requête pour créer un nouveau livre.

 mutation {
  livre (titre: "GRAND Stack", auteurs: ["James Blunt"]) {
    Titre
  }
}

Cette fois-ci, nous obtenons une réponse avec le titre du livre.

C’est une conclusion! ?

Woohoo! Nous avons maintenant une API en temps réel qui permet les opérations CRUD et exige que les clients soient authentifiés pour effectuer certaines opérations. Nous avons construit notre propre système d'authentification en stockant les informations utilisateur dans la base de données et en chiffrant le mot de passe à l'aide de bscriptjs . L'objet de contexte, qui est transmis à chaque résolveur, comprend désormais de nouvelles propriétés pour déterminer si la demande est authentifiée ainsi qu'un objet utilisateur. Vous pouvez accéder à l'objet utilisateur à partir de n'importe quel résolveur et vous en aurez peut-être besoin pour stocker plus d'informations (par exemple en ajoutant une nouvelle propriété pour déterminer quel utilisateur a créé ou mis à jour les données du livre). Nous avons ajouté une fonction wrapper que vous pouvez utiliser pour envelopper n'importe quel résolveur qui autorise l'accès uniquement aux utilisateurs authentifiés. Cette approche d'utilisation d'une fonction wrapper est similaire à l'utilisation d'un middleware. J'entrerai plus en détail sur le middleware GraphQL dans un prochain article.

J'espère que vous avez aimé lire ceci. N'hésitez pas à laisser tomber vos questions dans les commentaires. Vous pouvez trouver le code sur GitHub .

Happy Coding! ?





Source link