GraphQL API's beveiligen: Gebruikersauthenticatie implementeren in Express.js met behulp van JWT's
GraphQL is een populair alternatief voor de traditionele RESTful API-architectuur en biedt een flexibele en efficiënte query- en manipulatietaal voor gegevens voor API’s. Nu GraphQL steeds meer wordt gebruikt, wordt het steeds belangrijker om prioriteit te geven aan de beveiliging van GraphQL API’s om toepassingen te beschermen tegen ongeautoriseerde toegang en mogelijke inbreuken op gegevens.
Een haalbare strategie voor het beveiligen van GraphQL API’s omvat het gebruik van JSON Web Tokens (JWT’s), die een effectieve manier bieden om toegang te verlenen tot beperkte bronnen en bevoorrechte operaties uit te voeren, terwijl de communicatie tussen clients en API’s veilig blijft.
Authenticatie en Autorisatie in GraphQL API’s
In tegenstelling tot RESTful Application Programming Interfaces (API’s), hebben GraphQL API implementaties vaak een enkel eindpunt dat klanten in staat stelt om verschillende hoeveelheden en typen informatie op te vragen via hun queries. Deze aanpasbaarheid is zowel een voordeel als een potentiële bron van beveiligingsrisico’s, vooral met betrekking tot gecompromitteerde toegangscontrolemechanismen.
Om de bovengenoemde dreiging effectief te beheersen, is het cruciaal om uitgebreide authenticatie- en autorisatieprotocollen op te stellen, waaronder het nauwkeurig afbakenen van toegangsprivileges. Met dergelijke maatregelen zorgen we ervoor dat alleen bevoegde personen toegang krijgen tot gevoelige bedrijfsmiddelen, waardoor de kans op beveiligingsproblemen en incidenten met het lekken van gegevens tot een minimum wordt beperkt.
[voeg hier GitHub repository link in].
Een Express.js Apollo Server opzetten
Apollo Server is een veelgebruikte GraphQL serverimplementatie voor GraphQL APIs. Je kunt het gebruiken om eenvoudig GraphQL schema’s te bouwen, resolvers te definiëren en verschillende gegevensbronnen voor je API’s te beheren.
Om een Express.js Apollo Server op te zetten moet een projectmap worden aangemaakt en geopend.
mkdir graphql-API-jwt
cd graphql-API-jwt
Voer het volgende commando uit om een nieuw Node.js project te starten met behulp van de Node Package Manager (npm):
npm init --yes
Installeer nu deze pakketten.
npm install apollo-server graphql mongoose jsonwebtokens dotenv
Het is inderdaad essentieel om een server.js
bestand te genereren in de primaire map van het project en de server als volgt te configureren met behulp van het meegeleverde script:
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();
const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({ req }),
});
const MONGO_URI = process.env.MONGO_URI;
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connected to DB");
return server.listen({ port: 5000 });
})
.then((res) => {
console.log(`Server running at ${res.url}`);
})
.catch(err => {
console.log(err.message);
});
De GraphQL server is geconfigureerd met een combinatie van typeDefs
en resolvers
, die het schema en de operaties definiëren die de API kan verwerken.Bovendien maakt het gebruik van de context
optie het mogelijk om het req
object zo te configureren dat het de specifieke context bevat die relevant is voor elke individuele resolver. Hierdoor heeft de server toegang tot verzoekspecifieke informatie, inclusief headerwaarden, om het ophalen van gegevens efficiënter te laten verlopen.
Een MongoDB-database aanmaken
Ofwel maak je een nieuwe database binnen je lokale installatie van MongoDB, ofwel maak je gebruik van de cloud-gebaseerde oplossing van MongoDB Atlas door een cluster op te zetten en de nodige verbindingsinformatie te verkrijgen. Zodra je de juiste URI-string voor de verbinding hebt verkregen, kun je een nieuw .env
bestand maken en de verbindingsgegevens in het opgegeven formaat invoeren.
MONGO_URI="<mongo_connection_uri>"
Definieer het datamodel
Zeker, hier is een voorbeeld van hoe je een datamodel voor gebruikers definieert in een Mongoose schema bestand genaamd models/user.js
:javascriptconst mongoose = require(‘mongoose’);// Definieer het gebruikersschemaconst userSchema = new mongoose.Schema({name: {type: String, required: true },email: {type: String, unique: true, required: true },password: {type: String, required: true },});// Compileer het schema in een modelconst Gebruiker = mongoose.model(‘Gebruiker’, gebruikersSchema);module.exports = Gebruiker;In dit voorbeeld importeren we eerst de Mongoose bibliotheek bovenaan het bestand. Vervolgens maken we een nieuw Schema-object
const {model, Schema} = require('mongoose');
const userSchema = new Schema({
name: String,
password: String,
role: String
});
module.exports = model('user', userSchema);
Het GraphQL Schema definiëren
Het GraphQL API schema bepaalt de indeling van de informatie die kan worden opgevraagd, naast het afbakenen van de mogelijke operaties (aanvragen en wijzigingen) die kunnen worden uitgevoerd om te communiceren met gegevens via de genoemde interface.
Om een schema op te stellen voor je project, moet je eerst een nieuwe map aanmaken in de hoofdmap van je onderneming, die “graphql” wordt genoemd. Deze specifieke map zal dienen als een enclave waarin alle componenten met betrekking tot GraphQL operaties worden georganiseerd. Binnen de grenzen van de map ‘graphql’ moeten er twee verschillende documenten bestaan: een met de naam ’typeDefs.js’ en een met de naam ‘resolvers.js’.
Neem in het bestand typeDefs.js
de volgende code op:
const { gql } = require("apollo-server");
const typeDefs = gql`
type User {
id: ID!
name: String!
password: String!
role: String!
}
input UserInput {
name: String!
password: String!
role: String!
}
type TokenResult {
message: String
token: String
}
type Query {
users: [User]
}
type Mutation {
register(userInput: UserInput): User
login(name: String!, password: String!, role: String!): TokenResult
}
`;
module.exports = typeDefs;
Resolvers maken voor de GraphQL API
Resolver-functies spelen een essentiële rol bij het bepalen hoe gegevens worden verkregen in antwoord op verzoeken van klanten om informatie en wijzigingen, inclusief eventuele aanvullende details die in het schema zijn gespecificeerd.Bij ontvangst van een verzoek van een client activeert de GraphQL server de relevante resolvers om gegevens op te halen uit meerdere bronnen, waaronder databases en externe diensten, en stuurt deze dienovereenkomstig terug.
Om authenticatie en toegangscontrole met behulp van JSON Web Token (JWT) in onze GraphQL server mogelijk te maken, moeten we eerst resolvers instellen voor zowel de ‘registreren’ als de ‘inloggen’ mutaties. Deze resolvers zijn verantwoordelijk voor het beheer van de procedures voor het aanmaken en verifiëren van accounts. Vervolgens moeten we een resolver voor gegevens ophalen ontwikkelen die alleen toegankelijk is voor geregistreerde en geverifieerde gebruikers die hiervoor toestemming hebben gekregen.
RSA JWE encoder en decoder uit het ’node-jose’ pakket, evenals de ‘crypto’ module die standaard wordt geleverd door Node.js. Hierdoor kunnen we cryptografische bewerkingen met betrekking tot het genereren en valideren van JWT’s naadloos uitvoeren. Op deze manier kunnen we zorgen voor veilige communicatie tussen onze applicatie en haar gebruikers, met behoud van eenvoud en gebruiksgemak voor zowel ontwikkelaars als eindgebruikers.
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
Zorg ervoor dat u de geheime sleutel die nodig is voor het ondertekenen van JSON Web Tokens in uw .env-bestand opneemt, zodat u deze correct kunt gebruiken.
SECRET_KEY = '<my_Secret_Key>';
Om een geldig authenticatietoken te produceren, is het essentieel om de meegeleverde functie te gebruiken en aangepaste kenmerken, waaronder de vervaltijd, binnen het JWT-token te specificeren. Bovendien kunnen extra attributen, zoals de uitgiftetijd, worden opgenomen op basis van de specifieke behoeften van de toepassing.
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);
return token;
}
Om de authenticiteit van de JSON Web Tokens (JWT’s) die gebruikt worden binnen toekomstige HTTP-uitwisselingen te garanderen, is het noodzakelijk om een validatieproces voor deze tokens in te bouwen. Dit wordt bereikt door een authenticatiemechanisme te implementeren dat de legitimiteit van elke JWT die tijdens dergelijke interacties wordt gepresenteerd, controleert en bevestigt.
function verifyToken(token) {
if (!token) {
throw new Error('Token not provided');
}
try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
return decoded;
} catch (err) {
throw new Error('Invalid token');
}
}
De gegeven functie accepteert een token als invoer en gebruikt de aangewezen geheime sleutel om de authenticiteit ervan te valideren. Als het token legitiem blijkt te zijn, retourneert de functie het gedecodeerde token; in het geval van afwijkingen wordt echter een uitzondering weergegeven die de aanwezigheid van een ongeldig token aangeeft.
Definieer de API Resolvers
Om het resolverproces voor onze GraphQL API af te bakenen, moeten we de specifieke taken of acties specificeren die het zal uitvoeren, zoals het beheren van gebruikersregistratie en aanmeldingsprocessen. Maak in eerste instantie een object met resolverfuncties die dienen om deze aangewezen taken uit te voeren.Implementeer vervolgens de volgende mutaties om deze bewerkingen te vergemakkelijken:
const resolvers = {
Mutation: {
register: async (_, { userInput: { name, password, role } }) => {
if (!name || !password || !role) {
throw new Error('Name password, and role required');
}
const newUser = new User({
name: name,
password: password,
role: role,
});
try {
const response = await newUser.save();
return {
id: response._id,
...response._doc,
};
} catch (error) {
console.error(error);
throw new Error('Failed to create user');
}
},
login: async (_, { name, password }) => {
try {
const user = await User.findOne({ name: name });
if (!user) {
throw new Error('User not found');
}
if (password !== user.password) {
throw new Error('Incorrect password');
}
const token = generateToken(user);
if (!token) {
throw new Error('Failed to generate token');
}
return {
message: 'Login successful',
token: token,
};
} catch (error) {
console.error(error);
throw new Error('Login failed');
}
}
},
De register mutatie is verantwoordelijk voor het afhandelen van het registratieproces door het toevoegen van nieuwe gebruikersgegevens aan de database. De aanmeldmutatie daarentegen handelt aanmeldingen van gebruikers af en genereert bij succesvolle authenticatie een JSON Web Token (JWT) en retourneert een succesbericht in het antwoord.
Om de toegang tot een query die gebruikersgegevens ophaalt te beperken tot geauthenticeerde en bevoorrechte gebruikers met een “beheerdersrol”, is het essentieel om autorisatielogica in het systeem op te nemen. Dit zorgt ervoor dat onbevoegde personen geen toegang kunnen krijgen tot gevoelige informatie.
In essentie zal het onderzoek in eerste instantie de legitimiteit van het token valideren, gevolgd door een beoordeling van de rol van de gebruiker. In het geval dat het autorisatieonderzoek gunstig uitvalt, zal het resolutieverzoek de informatie van de eindgebruiker ophalen uit het archief.
Query: {
users: async (parent, args, context) => {
try {
const token = context.req.headers.authorization || '';
const decodedToken = verifyToken(token);
if (decodedToken.role !== 'Admin') {
throw new ('Unauthorized. Only Admins can access this data.');
}
const users = await User.find({}, { name: 1, _id: 1, role:1 });
return users;
} catch (error) {
console.error(error);
throw new Error('Failed to fetch users');
}
},
},
};
Start ten slotte de ontwikkelingsserver:
node server.js
Het wordt inderdaad ten zeerste aanbevolen om de werking van onze GraphQL API te valideren door gebruik te maken van de Apollo Server API sandbox in uw webbrowser. Om dit proces te illustreren, overweeg het gebruik van de “register” mutatie om nieuwe gebruikersinformatie in de database in te voeren, gevolgd door de “login” mutatie voor authenticatie doeleinden.
Neem ten slotte het JWT-token op in de autorisatieheaderparameter en haal vervolgens gebruikersinformatie op uit de database door een query uit te voeren.
Beveiligen van GraphQL API’s
Authenticatie en autorisatie spelen een centrale rol in het waarborgen van de veiligheid van GraphQL API’s; de implementatie ervan alleen garandeert echter geen volledige bescherming. Het is essentieel om verdere veiligheidsmaatregelen op te nemen, zoals robuuste invoervalidatie en versleuteling van gevoelige informatie om een grondige beveiliging te bereiken.
Het implementeren van een allesomvattende beveiligingsstrategie is essentieel om je application programming interfaces (API’s) te beschermen tegen verschillende soorten bedreigingen die zich kunnen voordoen.