Lass uns unsere eigene Authentifizierungs-API mit Nodejs und GraphQL erstellen

Avatar of Deven Rathore
Deven Rathore am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Die Authentifizierung ist eine der schwierigsten Aufgaben für Entwickler, die gerade erst mit GraphQL beginnen. Es gibt viele technische Überlegungen, darunter, welcher ORM einfach einzurichten ist, wie sichere Tokens generiert und Passwörter gehasht werden und sogar welche HTTP-Bibliothek verwendet werden soll und wie sie zu verwenden ist.

In diesem Artikel konzentrieren wir uns auf die lokale Authentifizierung. Sie ist vielleicht die beliebteste Methode zur Handhabung der Authentifizierung auf modernen Websites und geschieht durch die Abfrage von E-Mail und Passwort des Benutzers (im Gegensatz zur Verwendung von beispielsweise Google Auth).

Darüber hinaus verwendet dieser Artikel Apollo Server 2, JSON Web Tokens (JWT) und Sequelize ORM, um eine Authentifizierungs-API mit Node zu erstellen.

Authentifizierung verwalten

Als in, ein Anmeldesystem

  • Authentifizierung identifiziert oder verifiziert einen Benutzer.
  • Autorisierung validiert die Routen (oder Teile der App), auf die der authentifizierte Benutzer Zugriff haben kann.

Der Ablauf zur Implementierung ist

  1. Der Benutzer registriert sich mit Passwort und E-Mail
  2. Die Anmeldedaten des Benutzers werden in einer Datenbank gespeichert
  3. Der Benutzer wird nach Abschluss der Registrierung zum Login weitergeleitet
  4. Dem Benutzer wird Zugriff auf bestimmte Ressourcen gewährt, wenn er authentifiziert ist
  5. Der Zustand des Benutzers wird in einem der Browser-Speichermedien (z. B. localStorage, Cookies, Session) oder JWT gespeichert.

Voraussetzungen

Bevor wir in die Implementierung einsteigen, hier sind ein paar Dinge, die Sie benötigen werden, um mitzukommen.

Abhängigkeiten

Das ist eine lange Liste, also legen wir los

  • Apollo Server: Ein Open-Source-GraphQL-Server, der mit jeder Art von GraphQL-Client kompatibel ist. Wir werden in diesem Projekt keinen Express für unseren Server verwenden. Stattdessen nutzen wir die Leistung von Apollo Server, um unsere GraphQL-API bereitzustellen.
  • bcryptjs: Wir möchten die Benutzerpasswörter in unserer Datenbank hashen. Deshalb verwenden wir bcrypt. Es stützt sich auf die getRandomValues-Schnittstelle der Web Crypto API, um sichere Zufallszahlen zu erhalten.
  • dotenv: Wir verwenden dotenv, um Umgebungsvariablen aus unserer .env-Datei zu laden. 
  • jsonwebtoken: Sobald der Benutzer angemeldet ist, enthält jede nachfolgende Anfrage die JWT, die es dem Benutzer ermöglicht, auf Routen, Dienste und Ressourcen zuzugreifen, die mit diesem Token gestattet sind. jsonwebtoken wird verwendet, um eine JWT zu generieren, die zur Authentifizierung von Benutzern verwendet wird.
  • nodemon: Ein Tool, das die Entwicklung von Node-basierten Anwendungen unterstützt, indem es die Node-Anwendung automatisch neu startet, wenn Änderungen im Verzeichnis erkannt werden. Wir möchten den Server nicht jedes Mal schließen und starten, wenn sich etwas in unserem Code ändert. Nodemon prüft jedes Mal Änderungen in unserer App und startet den Server automatisch neu.
  • mysql2: Ein SQL-Client für Node. Wir benötigen ihn, um uns mit unserem SQL-Server zu verbinden, damit wir Migrationen ausführen können.
  • sequelize: Sequelize ist ein Promise-basierter Node ORM für Postgres, MySQL, MariaDB, SQLite und Microsoft SQL Server. Wir werden Sequelize verwenden, um unsere Migrationen und Modelle automatisch zu generieren.
  • sequelize cli: Wir werden Sequelize CLI verwenden, um Sequelize-Befehle auszuführen. Installieren Sie es global mit yarn add --global sequelize-cli im Terminal. 

Einrichtung der Verzeichnisstruktur und der Entwicklungsumgebung

Lass uns ein brandneues Projekt erstellen. Erstellen Sie einen neuen Ordner und fügen Sie Folgendes hinzu

yarn init -y

Das Flag -y bedeutet, dass wir bei allen Fragen von yarn init Ja auswählen und die Standardeinstellungen verwenden.

Wir sollten auch eine package.json-Datei in den Ordner legen, also installieren wir die Projekt-Abhängigkeiten

yarn add apollo-server bcrpytjs dotenv jsonwebtoken nodemon sequelize sqlite3

Als nächstes fügen wir Babel unserer Entwicklungsumgebung hinzu

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

Jetzt konfigurieren wir Babel. Führen Sie touch .babelrc im Terminal aus. Das erstellt und öffnet eine Babel-Konfigurationsdatei, in die wir Folgendes einfügen:

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

Es wäre auch schön, wenn unser Server startet und auch Daten migriert. Das können wir automatisieren, indem wir package.json damit aktualisieren:

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

Hier ist unsere package.json-Datei zu diesem Zeitpunkt in ihrer Gesamtheit:

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

Nachdem nun unsere Entwicklungsumgebung eingerichtet ist, wenden wir uns der Datenbank zu, in der wir Dinge speichern werden.

Datenbank-Einrichtung

Wir werden MySQL als unsere Datenbank und Sequelize ORM für unsere Beziehungen verwenden. Führen Sie sequelize init aus (vorausgesetzt, Sie haben es zuvor global installiert). Der Befehl sollte drei Ordner erstellen: /config /models und /migrations. Zu diesem Zeitpunkt nimmt unsere Projektverzeichnisstruktur Gestalt an.

Konfigurieren wir unsere Datenbank. Erstellen Sie zuerst eine .env-Datei im Stammverzeichnis des Projekts und fügen Sie Folgendes ein:

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

Gehen Sie dann zum Ordner /config, den wir gerade erstellt haben, und benennen Sie die Datei config.json darin in config.js um. Fügen Sie dann diesen Code ein:

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
}

Hier lesen wir die Datenbankdetails aus, die wir in unserer .env-Datei festgelegt haben. process.env ist eine globale Variable, die von Node injiziert wird, und sie wird verwendet, um den aktuellen Zustand der Systemumgebung darzustellen.

Aktualisieren wir unsere Datenbankdetails mit den entsprechenden Daten. Öffnen Sie die SQL-Datenbank und erstellen Sie eine Tabelle namens graphql_auth. Ich verwende Laragon als meinen lokalen Server und phpmyadmin zur Verwaltung von Datenbanktabellen.

Was auch immer Sie verwenden, wir werden die .env-Datei mit den neuesten Informationen aktualisieren.

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

Konfigurieren wir Sequelize. Erstellen Sie eine .sequelizerc-Datei im Stammverzeichnis des Projekts und fügen Sie Folgendes ein:

const path = require('path')


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

Jetzt integrieren wir unsere Konfiguration in die Modelle. Gehen Sie zu index.js im Ordner /models und bearbeiten Sie die config-Variable.

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

Schließlich schreiben wir unser Modell. Für dieses Projekt benötigen wir ein User-Modell. Lassen Sie uns Sequelize verwenden, um das Modell automatisch zu generieren. Hier ist, was wir im Terminal ausführen müssen, um das einzurichten:

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

Lassen Sie uns das Modell bearbeiten, das für uns erstellt wird. Gehen Sie zu user.js im Ordner /models und fügen Sie Folgendes ein:

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

Hier haben wir Attribute und Felder für Benutzername, E-Mail und Passwort erstellt. Lassen Sie uns eine Migration ausführen, um Änderungen an unserem Schema zu verfolgen.

yarn migrate

Schreiben wir jetzt das Schema und die Resolver.

Schema und Resolver mit dem GraphQL-Server integrieren

In diesem Abschnitt definieren wir unser Schema, schreiben Resolverfunktionen und stellen sie auf unserem Server bereit.

Das Schema

Erstellen Sie im src-Ordner einen neuen Ordner namens /schema und darin eine Datei namens schema.js. Fügen Sie den folgenden Code ein:

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

Hier haben wir graphql-tag von apollo-server importiert. Apollo Server erfordert das Umhüllen unseres Schemas mit gql.

Die Resolver

Erstellen Sie im src-Ordner einen neuen Ordner namens /resolvers und darin eine Datei namens resolver.js. Fügen Sie den folgenden Code ein:

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

Das ist viel Code, also sehen wir uns an, was dort passiert.

Zuerst haben wir unsere Modelle, bcrypt und  jsonwebtoken importiert und dann unsere Umgebungsvariablen initialisiert.

Als nächstes kommen die Resolverfunktionen. Im Query-Resolver haben wir drei Funktionen (me, user und allUsers)

  • Die me-Abfrage ruft die Details des aktuell eingelogten Benutzers ab. Sie akzeptiert ein user-Objekt als Kontext-Argument. Der Kontext wird verwendet, um Zugriff auf unsere Datenbank zu gewähren, die verwendet wird, um die Daten für einen Benutzer anhand der ID zu laden, die als Argument in der Abfrage angegeben wird.
  • Die user-Abfrage ruft die Details eines Benutzers anhand seiner ID ab. Sie akzeptiert id als Kontext-Argument und ein user-Objekt.
  • Die alluser-Abfrage gibt die Details aller Benutzer zurück.

user wäre ein Objekt, wenn der Benutzerstatus eingeloggt wäre, und null, wenn nicht. Wir würden diesen Benutzer in unseren Mutationen erstellen.

Im Mutations-Resolver haben wir zwei Funktionen (registerUser und loginUser)

  • registerUser akzeptiert den username, die email  und das password des users und erstellt eine neue Zeile mit diesen Feldern in unserer Datenbank. Es ist wichtig zu beachten, dass wir das bcryptjs-Paket verwendet haben, um das Passwort des Benutzers mit bcrypt.hash(password, 10) zu hashen. jsonwebtoken.sign signiert die gegebene Nutzlast synchron in einen JSON Web Token-String (in diesem Fall die Benutzer-id und email). Schließlich gibt registerUser den JWT-String und das Benutzerprofil zurück, wenn erfolgreich, und gibt eine Fehlermeldung zurück, wenn etwas schiefgeht.
  • login akzeptiert email und password und prüft, ob diese Details mit den angegebenen übereinstimmen. Zuerst prüfen wir, ob der email-Wert bereits irgendwo in der Benutzerdatenbank vorhanden ist.
models.User.findOne({ where: { email }})
if (!user) {
  throw new Error('No user with that email')
}

Dann verwenden wir die bcrypt.compare-Methode von bcrypt, um zu prüfen, ob das Passwort übereinstimmt.

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

Dann, genau wie wir es zuvor in registerUser getan haben, verwenden wir jsonwebtoken.sign, um einen JWT-String zu generieren. Die login-Mutation gibt den Token und das user-Objekt zurück.

Fügen wir nun den JWT_SECRET zu unserer .env-Datei hinzu.

JWT_SECRET=somereallylongsecret

Der Server

Endlich der Server! Erstellen Sie eine server.js-Datei im Stammverzeichnis des Projekts und fügen Sie diesen Code ein:

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

Hier importieren wir das Schema, die Resolver und jwt und initialisieren unsere Umgebungsvariablen. Zuerst verifizieren wir den JWT-Token mit verify. jwt.verify akzeptiert den Token und das JWT-Secret als Parameter.

Als nächstes erstellen wir unseren Server mit einer ApolloServer-Instanz, die typeDefs und Resolver akzeptiert.

Wir haben einen Server! Lassen Sie ihn laufen, indem Sie yarn dev im Terminal ausführen.

Testen der API

Lassen Sie uns nun die GraphQL-API mit GraphQL Playground testen. Wir sollten uns registrieren, anmelden und alle Benutzer anzeigen können – einschließlich eines einzelnen Benutzers – per ID.

Wir beginnen damit, die GraphQL Playground-App zu öffnen oder einfach localhost://4000 im Browser zu öffnen, um darauf zuzugreifen.

Mutation für Benutzerregistrierung

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

Wir sollten etwas wie das hier erhalten.

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

Mutation für Login

Lassen Sie uns nun mit den gerade erstellten Benutzerdaten anmelden.

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

Wir sollten etwas wie das hier erhalten.

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

Fantastisch!

Abfrage für einen einzelnen Benutzer

Um einen einzelnen Benutzer abzufragen, müssen wir den Benutzertoken als Autorisierungsheader übergeben. Gehen Sie zum Reiter HTTP Headers.

Showing the GraphQL interface with the HTTP Headers tab highlighted in red in the bottom left corner of the screen,

...und fügen Sie dies ein:

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

Hier ist die Abfrage:

query myself{
  me {
    id
    email
    username
  }
}

Und wir sollten etwas wie das hier erhalten:

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

Großartig! Holen wir uns nun einen Benutzer nach ID.

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

Und hier ist die Abfrage, um alle Benutzer zu erhalten.

{
  allUsers{
    id
    username
    email
  }
}

Zusammenfassung

Die Authentifizierung ist eine der schwierigsten Aufgaben beim Erstellen von Websites, die sie erfordern. GraphQL ermöglichte es uns, eine gesamte Authentifizierungs-API mit nur einem Endpunkt zu erstellen. Sequelize ORM macht die Erstellung von Beziehungen zu unserer SQL-Datenbank so einfach, dass wir uns kaum um unsere Modelle kümmern mussten. Es ist auch bemerkenswert, dass wir keine HTTP-Server-Bibliothek (wie Express) benötigen und Apollo GraphQL als Middleware verwenden. Apollo Server 2 ermöglicht es uns nun, unsere eigenen bibliotheksunabhängigen GraphQL-Server zu erstellen!

Sehen Sie sich den Quellcode für dieses Tutorial auf GitHub an.