Skip to main content

Eine sichere GraphQL-API mit Node.js entwickeln

Artikel von
Headshot of Lawrence Eagles

Lawrence Eagles

blog banner node js

29. März 2022

0 Min. Lesezeit

GraphQL bietet dank Validierung und Typprüfung bereits standardmäßig Sicherheitsfunktionen. Allerdings deckt es nicht alle Sicherheitsrisiken im Zusammenhang mit APIs ab. In diesem Artikel erfahren Sie, wie Sie GraphQL-APIs absichern, indem Sie eine einfache Node.js-Anwendung mit Fastify und GraphQL erstellen.

Laut der offiziellen Dokumentation ist GraphQL eine Graph-Abfragesprache für APIs und eine Laufzeitumgebung, die diese Abfragen mit unseren Daten ausführt. GraphQL beschreibt die Daten in unserer API klar und ermöglicht es uns, schnelle und flexible APIs zu erstellen. Gleichzeitig haben Clients die volle Kontrolle darüber, welche Daten sie benötigen. Wie REST läuft GraphQL über HTTP, ist daher datenbankunabhängig und funktioniert mit jeder Backend-Sprache und jedem Client.

Fastify ist ein pluginbasiertes, hocheffizientes und besonders performantes Node.js-Framework für die Entwicklung schneller HTTP-Server. Fastify ist von Hapi und Express inspiriert und bietet eine entwicklerfreundlichere und leistungsstärkere Alternative mit geringem Overhead.

Fastify unterstützt GraphQL über das Mercurius-Plugin. Das Mercurius-Plugin ist ein konfigurierbarer GraphQL-Adapter für Fastify, den wir in den folgenden Abschnitten näher kennenlernen.

Beginnen wir mit den Voraussetzungen.

Voraussetzungen

Für diesen Artikel benötigen Sie Folgendes:

  • Node.js Version 12 oder höher

  • Grundkenntnisse in JavaScript

  • Grundkenntnisse in GraphQL

Erste Schritte

Zunächst erstellen wir einen einfachen Node.js-Server.

Erstellen Sie ein Projekt, indem Sie einen Projektordner anlegen. Führen Sie in diesem Ordner den folgenden Code in der Kommandozeile (CLI) aus. Dadurch wird unsere Anwendung eingerichtet und die erforderlichen Abhängigkeiten werden installiert.

// bootstrap npm project
npm init -y

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

Dieses Projekt verwendet eine bestimmte Mercurius-Version. Installieren Sie sie mit folgendem Befehl:

npm i mercurius@7.9.1

Als Nächstes aktivieren wir ES6-Module, damit wir statt commonJS das standardmäßige JavaScript-Modulsystem verwenden können. Fügen Sie dazu "type": "module" in die Datei package.json ein.

Anschließend aktualisieren wir die NPM-Skripte um einen Befehl zum Starten des Node.js-Servers. Öffnen Sie dazu die Datei package.json und bearbeiten Sie den Abschnitt „scripts“ wie folgt:

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

Beachten Sie, dass der Codeausschnitt --es-module-specifier-resolution=node erforderlich ist, damit ES-Module und die commonJS-Module von Node interoperabel sind.

Erstellen Sie nun im Stammverzeichnis ein Verzeichnis src. Erstellen Sie darin einen Ordner graphql mit den Dateien schema.js und resolvers.js. Fügen Sie den folgenden Code in die Datei schema.js ein:

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

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

Fügen Sie nun den folgenden Code in die Datei resolvers.js ein:

const resolvers = {};
export default resolvers;

Im nächsten Abschnitt aktualisieren wir schema.js und ergänzen die Datei resolvers.js. Wir müssen die Dateien jedoch jetzt schon mit etwas Boilerplate-Code erstellen, da unser Server sie benötigt, um korrekt zu funktionieren.

Erstellen Sie im Verzeichnis src eine Datei index.js und fügen Sie folgenden Code ein:

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();

Der obige Code erstellt einen grundlegenden Fastify-Server und registriert das Mercurius-Plugin mit den Optionen schema, resolvers, graphiql und queryDepth.

Starten Sie den Server nun mit npm run dev. Die Ausgabe sieht folgendermaßen aus:

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

Wie Sie sehen, funktioniert unser Server. Im nächsten Abschnitt erstellen wir unsere Blog-APIs mit GraphQL.

Eine sichere Blog-API mit Fastify und GraphQL entwickeln

Es gibt verschiedene Strategien, um eine API abzusichern. Dazu gehören:

  • Authentifizierung und Autorisierung: Bei der Authentifizierung wird überprüft, ob eine Person tatsächlich diejenige ist, für die sie sich ausgibt. Bei der Autorisierung geht es um Berechtigungen. Die Authentifizierung entscheidet, ob sich eine Person anmelden kann, und merkt sich anschließend diese Person. Die Autorisierung legt fest, welche Berechtigungen einem identifizierten Benutzer zugewiesen sind. Sie bestimmt damit, ob dieser beispielsweise Daten erstellen, lesen, aktualisieren oder löschen darf.

  • Fehler maskieren: Die genauen Informationen zu einem Serverfehler werden zurückgehalten, um zu verhindern, dass dem Client unbeabsichtigt Details mitgeteilt werden, die Schwachstellen des Servers offenlegen könnten.

  • Begrenzung der Abfragetiefe: Hierbei wird eine maximale Tiefe für GraphQL-Abfragen festgelegt. Tief verschachtelte Abfragen sind gefährlich, da sie viele Ressourcen benötigen und aufwendig zu berechnen sind. Dadurch können unsere APIs abstürzen.

  • Eingaben bereinigen und validieren: Verwenden Sie gängige Web-Sicherheitstechniken, um zu verhindern, dass Benutzer schädliche Daten senden. In unserer Anwendung nutzen wir die integrierte GraphQL-Validierung.

In diesem Artikel entwickeln und sichern wir unsere APIs mithilfe der oben genannten Strategien sowie Mercurius und des Mercurius-Auth-Plugins.

Das Mercurius-Auth-Plugin bietet für unsere Zwecke zwei wichtige Funktionen. Erstens können wir damit benutzerdefinierte Auth-Direktiven für Felder in unserem Schema definieren. Auth-Direktiven sind Zeichenfolgen, die als Bezeichner für geschützte Felder in unserem Schema dienen.

Außerdem können wir bei einer GraphQL-Anfrage benutzerdefinierte Auth-Richtlinien auf diese geschützten Felder anwenden.

Zunächst erstellen wir Beispieldaten. Erstellen Sie im Verzeichnis src einen Ordner data mit einer Datei index.js, die den folgenden Code enthält:

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

Als Nächstes richten wir unser schema ein, indem wir den Boilerplate-Code in der Datei schema.js durch folgenden Code ersetzen:

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;

Im obigen Code haben wir unser GraphQL-Schema erstellt und Auth-Direktiven für die Felder user und users definiert. Gleich wenden wir benutzerdefinierte Richtlinien auf diese geschützten Felder an.

Nun fügen wir unsere Resolver hinzu, indem wir den Boilerplate-Code in der Datei resolvers.js durch folgenden Code ersetzen:

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;

Der obige Code enthält Resolver für die Abfragen user, users und login.

Abschließend müssen wir eine benutzerdefinierte Auth-Richtlinie hinzufügen, indem wir das Mercurius-Auth-Plugin registrieren. Öffnen Sie dazu die Datei index.js im Verzeichnis src und fügen Sie den folgenden Code unter dem Kommentar register auth policy in Zeile 19 ein:

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

In unserer benutzerdefinierten Richtlinie ruft die Methode authContext das Benutzer-Token aus den Headern ab. Die Methode applyPolicy enthält benutzerdefinierte Richtlinien für Authentifizierung und Autorisierung.

Schlägt die Authentifizierung oder Autorisierung eines Benutzers fehl, geben wir außerdem einen Fehler mit einer allgemeinen Meldung aus, etwa „Ein Fehler ist aufgetreten. Versuchen Sie es erneut!“. Diese Meldung wird anstelle einer detaillierten Serverfehlermeldung angezeigt, die bestehende Schwachstellen des Servers offenlegen könnte.

Damit sind wir fertig. Im nächsten Abschnitt testen wir unsere APIs.

API testen

Starten Sie zunächst den Server, indem Sie im Stammverzeichnis npm run dev ausführen. Rufen Sie anschließend den GraphQL-Playground unter http://localhost:4500/playground auf.

Wenn wir nun eine geschützte API wie users oder user abfragen, erhalten wir den unten gezeigten Fehler:

GraphQL-Oberfläche mit einer users-Abfrage, die Benutzerdetails anfordert, und einer Fehlermeldung: „Ein Fehler ist aufgetreten. Versuchen Sie es erneut!“

Damit unsere Abfrage erfolgreich ist, müssen wir uns also authentifizieren. Melden wir uns an, um ein Token zu erhalten.

Öffnen Sie zum Anmelden einen neuen Tab im Playground und führen Sie die folgende Abfrage aus:

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

Bei erfolgreicher Abfrage wird ein Token erstellt und wie in der folgenden Abbildung zurückgegeben.

GraphQL-Oberfläche mit einer Login-Abfrage, Feldern für Benutzername und Passwort sowie einem zurückgegebenen Authentifizierungstoken

Beachten Sie, dass die Anmeldedaten des Benutzers bereits in der Datei index.js im Verzeichnis data enthalten sind.

Wenn Sie versuchen, sich mit Benutzerdaten anzumelden, die nicht in dieser Datei enthalten sind (z. B. mit dem Benutzernamen „John1Doe“), tritt der unten gezeigte Fehler auf:

GraphQL-Oberfläche mit einer Login-Abfrage, die Felder für Benutzername und Passwort enthält und den Fehler „unbekannter Benutzer!“ zurückgibt

Wenn wir unser Token nun als x-user im Header übergeben, können wir unsere geschützten APIs wie unten gezeigt erfolgreich abfragen. Kopieren Sie dazu das Token aus der Anmeldeabfrage und verwenden Sie es als Wert für x-user.

users-Abfrage

Hier sehen Sie ein Beispiel für eine users-Abfrage:

query {
  users {
    id
    username
    password
    email
    role
  }
}

Fügen Sie Ihr Token wie unten gezeigt als Wert des HTTP-Header-Parameters x-user hinzu:

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

Das führt zu folgender Ausgabe:

GraphQL-Abfrageoberfläche mit einer Users-Abfrage, einem HTTP-Autorisierungsheader und zurückgegebenen Benutzerdaten mit Benutzernamen, Passwörtern, E-Mail-Adressen und Rollen.

user-Abfrage

Hier sehen Sie ein Beispiel für eine user-Abfrage:

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

Fügen Sie Ihr Token wie unten gezeigt als Wert des HTTP-Header-Parameters x-user hinzu:

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

Das führt zu folgender Ausgabe:

GraphQL-Playground mit einer Benutzerabfrage, einem HTTP-Autorisierungsheader und zurückgegebenen Benutzerdaten mit Benutzername, E-Mail-Adresse, Passwort und Admin-Rolle

Fazit

In diesem Artikel haben wir gesehen, wie einfach sich GraphQL-APIs absichern lassen, wenn wir mit Fastify und GraphQL eine einfache Node.js-Anwendung erstellen.

Wie besprochen, bietet GraphQL integrierte Sicherheitsfunktionen wie Validierung und Typprüfung. Da Benutzer dank der Flexibilität und Leistungsfähigkeit nach Belieben Daten anfordern können, sollte Sicherheit jedoch immer höchste Priorität haben.

In diesem Artikel haben wir außerdem einige Strategien zur Absicherung von GraphQL-APIs vorgestellt: Authentifizierung und Autorisierung, Begrenzung der Abfragetiefe, Maskierung von Fehlern sowie Bereinigung und Validierung von Eingaben.

Diese Sicherheitsstrategien sind sehr wirksam. Zusätzliche Sicherheit bieten weitere Maßnahmen wie Abfrage-Timeouts und Ratenbegrenzungen. Sie legen fest, wie häufig ein Client eine API innerhalb eines bestimmten Zeitraums abfragen darf.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.

Testen Sie unser kostenloses Online-Tool JavaScript-Code-Prüfer, um zu sehen, wie die Snyk-Code-Engine Ihren Code auf Sicherheits- und Qualitätsprobleme untersucht.