5. Sessions
5. Sessions
Tijdens deze les bespreken we hoe een API beveiligd kan worden door middel van klassieke session tokens en hoe we een onderscheid kunnen maken tussen verschillende rollen. Deze concepten worden geïllustreerd aan de hand van een API voor een webshop waar producten besteld kunnen worden door ingelogde gebruikers en waar administrators extra producten kunnen aanmaken.
Voor deze les worden startbestanden voorzien waarin de API-routes, het Prisma schema en de DAL-laag reeds gegeven zijn. Het is vanzelfsprekend dat we bepaalde routes nog wel zullen uitbreiden. Vergeet ook zeker niet om een migration uit te voeren voordat je het lesvoorbeeld nabouwt.
Startbestanden
Authenticatie
Authenticatie via sessions werkt op basis van een session token die op de server gegenereerd wordt, en vervolgens naar de client gestuurd wordt in de vorm van een cookie. De session token wordt niet enkel op de client bewaard maar ook op de server. Dit betekent dat de server steeds kan (en moet) valideren of de token die de client stuurt overeenkomt met de token in de database. Als de token overeenkomt, moet ook gecontroleerd worden of de session nog actief is, als dit het geval is de gebruiker geautoriseerd en kan deze de applicatie gebruiken.
Database schema
Het databaseschema dat al beschikbaar is in de startbestanden bevat de tabellen User en Session. In de User tabel zijn de email en password kolommen voorzien en elke User heeft één of meerdere actieve sessions. We hebben duidelijk meerdere sessies per gebruiker nodig, anders is het niet mogelijk om in te loggen op verschillende toestellen.
Merk op dat we geen automatisch gegeneerd id gebruiken voor de Session tabel, we zullen de session token generen via de randomBytes methode uit Node, het zou echter ook mogelijk zijn om een uuid te gebruiken.
Merk op dat de activeUntil kolom standaard ingesteld wordt op 24 uur na het moment waarop een rij toegevoegd is. De sessie blijft best niet al te lang actief, maar natuurlijk mag deze ook niet te kort zijn. De meest veilige geldigheidstermijn is 0 seconden, maar dit is natuurlijk heel onaangenaam voor de gebruiker. Dit zou betekenen dat de gebruiker de inloggegevens met elk request moet meesturen (wat natuurlijk geëncrypteerd wordt via TLS op HTTPS verbindingen). De meest gebruiksvriendelijke optie is natuurlijk om de sessie voor altijd actief te houden, maar ook dit is niet ideaal omdat de sessie dan niet vervalt als het toestel waarop ingelogd is gestolen wordt of ergens achtergelaten wordt.
Het is duidelijk dat voor de levenduur van de sessie dus een balans gevonden moet worden tussen gebruiksvriendelijkheid en beveiliging. In deze cursus zullen we de sessie actief houden voor 24 uur, afhankelijk van het soort website kan dit verhoogt of verlaagt worden. Zo houdt Netflix een sessie actief voor 1 jaar en houden banken de sessie slechts actief totdat het tabblad gesloten wordt.
Merk ook op dat we voor de User relatie in de Session kolom een cascade relatie gebruikt hebben. Dit betekent dat alle sessies voor een bepaalde gebruiker verwijderd worden als een deze gebruiker verwijderd worden.
model User {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @unique @db.VarChar(255)
password String
role Role @relation(fields: [roleId], references: [id])
roleId String @db.Uuid
sessions Session[]
Order Order[]
}
model Session {
id String @id @unique @db.VarChar()
activeFrom DateTime @default(now())
activeUntil DateTime @default(dbgenerated("CURRENT_TIMESTAMP + interval '1 day'"))
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}Account aanmaken
Een account aanmaken is relatief eenvoudig. Er is reeds een POST /accounts route voorzien die de createUser methode uit de DAL oproept. Deze methode moet nog geïmplanteerd worden. Voordat we hieraan kunnen beginnen moeten we het wachtwoord hashen.
Het is vanzelfsprekend dat een wachtwoord niet als plain text bewaard mag worden, dit maakt het veel te eenvoudig voor kwaadwilligen om een gebruiken hun wachtwoord te weten te komen. Hiervoor moet de database niet eens gehackt worden, elke developer die toegang heeft tot de production database zou deze wachtwoorden kunnen uitlezen. In de plaats van plain text moeten de wachtwoorden gehasht worden, hiervoor zijn natuurlijk heel veel algoritmes beschikbaar. Enkele voorbeelden zijn MD5, SHA-1, SHA-512, bcrypt, scrypt, PBKDF2, agron2i, argon2d, ... Veel van deze algoritmes zijn niet meer veilig omdat hardware zodanig geëvolueerd is dat het eenvoudig wordt om het wachtwoord te achterhalen als de hash gekend is. In deze cursus zullen we gebruik maken van PBKDF2, een algoritme dat doorheen verschillende iteraties nieuwe hashes berekend. Doordat er meerdere iteraties zijn wordt het wachtwoord moeilijker te kraken en worden eventuele zwakke wachtwoorden gecompenseerd.
Waarschuwing
Deze cursus is geen cryptografisch advies, voordat je zelf een hashing algoritme kiest, is het aan te raden om een cyber-security expert te raadplegen. Het is, in veel situaties, beter om een authentication service of library te gebruiken die professioneel geaudit zijn dan zelf iets te ontwerpen.
Daarnaast is het algoritme dat we gebruiken niet meer de beste keuze, het is beter om argon2[1] te gebruiken, een algoritme waarin zowel de tijd als memory kost aanpasbaar is.
Voordat we het wachtwoord hashen wordt er eerst een salt toegevoegd. Een salt is een willekeurige string die uniek is voor elk wachtwoord in de database. Door deze te concateneren met het wachtwoord voordat dit gehasht wordt, worden er voor twee gebruikers die hetzelfde wachtwoord gebruiken een andere hash bewaard in de database. Naast deze obfuscatie heeft salt nog een ander bijzonder groot voordeel. Omdat elk wachtwoord gehasht is met een andere salt, wordt het onmogelijk om een rainbow table op te stellen. Dit is een tabel waar voor veel voorkomende wachtwoorden de hashes teruggevonden kunnen worden, zo kan een kwaadwillige zeer snel inloggen op verschillende accounts als deze toegang gekregen heeft tot User tabel uit de database. Omdat de salt uniek is per gebruiker, mag deze in de database bewaard worden. Zelfs als een hacker hier toegang tot krijgt, is dit niet erg, want er zijn zodanig veel mogelijke wachtwoorden dat het computationeel onmogelijk is om het wachtwoord te raden. Alle mogelijke wachtwoorden zouden dan met elke mogelijke hash gecombineerd moeten worden.
Als je nog een stap verder wilt gaan is het ook mogelijk om een pepper toe te voegen. Dit is een willekeurig genereerde string die met het wachtwoord en het salt gecombineerd wordt, maar die gelijk is voor elke gebruiker. De pepper wordt dan ook niet bewaard in de database, maar wel in environment variables. We gebruiken in deze les geen pepper.
We gebruiken PBKDF2 hieronder om het wachtwoord te hashen en vervolgens bewaren we deze hash in de database. De hash die gegenereerd wordt door de pbkdf2Sync methode bevat de salt nog niet, daarom voegen we deze (en de gebruikte parameters) toe in de string die naar de database bewaard wordt. Dit is cruciaal, als we de salt niet bewaren, wordt het onmogelijk om een wachtwoord te verifiëren, elke salt is tenslotte uniek.
Merk op dat we voor de twee opties die we meegeven aan de hashfunctie, enkele willekeurig gekozen waarden gebruiken. In een productieomgeving moeten deze waarden afgestemd worden zodat het niet te lang duurt om de hash te berekenen, maar zodat het toch veilig is. Als de iterations verhoogt, is de hash veiliger, maar duurt het langer om te berekenen.
In hashing worden regelmatig byte-arrays gebruikt in de plaats van een string interpretatie daarvan. Via de toString methode kunnen we zo'n array converteren naar een string. De 'hex' parameter geeft aan hoe de string geïnterpreteerd moet worden, maar heeft verder weinig invloed, we zouden hier evengoed kunnen kiezen om de string te interpreteren als een base64 string. Het enige verschil zijn de tekens die voor kunnen komen in het resultaat, in een hexadecimale string zijn slechts 16 karakters beschikbaar (1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f). Dit betekent dat het veilig is om een dollar teken te gebruiken om de hash en salt van elkaar te scheiden in de database, want het dollar teken zal nooit voorkomen in de hash.
import {pbkdf2Sync, randomBytes} from 'crypto'
const options = {
keyLength: 64,
iterations: 600000,
}
export async function hashPassword(password: string): Promise<string> {
const salt = getSalt().toString('hex')
const hash = pbkdf2Sync(password, salt, options.iterations, options.keyLength, 'sha512').toString('hex')
return `${options.iterations}$${options.keyLength}$${hash}$${salt}`
}
function getSalt(): Buffer {
return randomBytes(32)
}export async function createUser(userInput: IdOptional<User>): Promise<UserWithRole> {
const user = await prismaClient.user.create({
data: {
email: userInput.email,
password: await hashPassword(userInput.password),
role: {
connect: {
name: Roles.customer,
},
},
},
include: {
role: true,
},
})
return user
}Hieronder zijn twee voorbeeld hashes te zien (inclusief parameters en salt), deze zijn allebei gegenereerd op basis van het wachtwoord 'test123test'.
600000$64$812408e0846073cd7cad4dd9eab3b1d698993e0ce1a65c2cb45b86d74b030da4ba29655f90f664dcb6f0b4ffc00706f9571d7d1198aa3c37421f9c20233170a5$69a318662ab9f3d328aa5569f7ef08a05f4cbc96bb0274f5239ef2af6e3e34b4
600000$64$4f0626b6481e7d0159c92bed10be45f6891c8c2567864a6ec40f79cc81716631712cfc81e98540251c54af46c81328fd7aa0ff4b878196c4786c98219b98990c$906f56eb5dc4665c594ee3d51b9623ab8f6dfe680ad7e109c0cc1b04cfdb5465Inloggen
Aangezien de wachtwoorden als hashes bewaard zijn, kunnen we niet gewoon controleren of een wachtwoord voorkomt in de database. We moeten het gehashte wachtwoord en de bijhorende salt uitlezen zodat we het wachtwoord dat door de gebruiker ingegeven is kunnen hashen met dezelfde salt die origineel gebruikt is om het wachtwoord te bewaren in de database. We hebben, naast de hash, ook de gebruikte parameters bewaard, dit is handing in het geval dat we doorheen de levensduur van de applicatie de parameters moeten aanpassen omdat de beschikbare CPU power groeit.
export function verifyPassword(dbHash: string, password: string): boolean {
const [iterations, hashLength, hash, salt] = dbHash.split('$')
const passwordHash = pbkdf2Sync(password, salt, Number(iterations), Number(hashLength), 'sha512').toString('hex')
return passwordHash === hash
}Deze functie kan vervolgens gebruikt worden in de POST /auth/login route.
login.post('/login', async (req: Request<void, UserWithRole, IdOptional<User>>, res: Response<UserWithRole | string>) => {
const user = await getUserByEmail(req.body.email)
const validCredentials = user && verifyPassword(user.password, req.body.password)
if (!validCredentials) {
res.statusCode = 400
res.send('Invalid credentials')
return
}
res.send(user!)
})Onderstaande video toont dat het inloggen werkt, een geldige account wordt geaccepteerd, maar ongeldige inloggegevens worden afgewezen.
Sessie starten
Om een sessie te starten moeten we een nieuwe rij toevoegen in de Session tabel. We genereren eerst een aantal willekeurige bytes die vervolgens geconverteerd worden naar een string, vergelijkbaar met salt.
We integreren de startSession methode in de createUser methode zodat er onmiddellijk een sessie gestart wordt als een nieuwe gebruiker aangemaakt wordt. Tenslotte passen we het return-type van de createUser methode aan zodat er, naast het User object, ook een Session object teruggegeven wordt.
export async function startSession(userId: string): Promise<Session> {
const id = randomBytes(32).toString('hex')
return prismaClient.session.create({
data: {
userId,
id,
},
})
}export async function createUser(userInput: IdOptional<User>): Promise<[UserWithRole, Session]> {
const user = await prismaClient.user.create({
data: {
email: userInput.email,
password: await hashPassword(userInput.password),
role: {
connect: {
name: Roles.customer,
},
},
},
include: {
role: true,
},
})
const session = await startSession(user.id)
return [user, session]
}Deze startSession methode geeft een Session object terug dat vervolgens gebruikt kan worden om een cookie in te stellen. Aangezien het cookie ingesteld moet worden nadat een gebruiker registreert of inlogt en als de sessie verlengt wordt, zonderen we deze functionaliteit af in een aparte functie.
Merk op dat we een response object meegeven als parameter aan onderstaande functie, dit is nodig omdat een cookie gekoppeld is aan een specifiek request.
Het cookie krijgt als naam session als waarde het id van de sessie die in de database aangemaakt is. Verder worden er verschillende configuratieopties ingesteld, deze zijn allemaal cruciaal voor de veiligheid van het cookie.
httpOnly: Deze optie zorgt ervoor dat het cookie niet uitgelezen kan worden vanuit JavaScript, het cookie is enkel leesbaar vanuit de browser en natuurlijk het operating system waarop de browser draait.secure: Deze optie bepaald of het cookie al dan niet verstuurd mag worden over een HTTP-verbindingen. Aangezien het cookie gevoelige gegevens bevat, moet dit in een productieomgeving op true staan, zo worden enkel HTTPS verbinding toegestaan.sameSite: Deze optie bepaald wanneer de browser het cookie meestuurt met een HTTP request. Door dit op strict te zetten, wordt het cookie enkel meegestuurd met requests naar het domein dat het cookie ingesteld heeft.path: Geeft aan voor welke pagina's op de website het cookie beschikbaar is.expires: Geef aan hoe lang het cookie beschikbaar blijft, na dit tijdstip wordt het cookie automatisch verwijderd.
import {Response} from 'express'
import {Session} from '@prisma/client'
const cookieName = 'session'
export function setSessionCookie(res: Response, session: Session): void {
res.cookie(
cookieName,
session.id,
{
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
expires: session.activeUntil,
},
)
}Nu dat de setSessionCookie methode beschikbaar is, kunnen we deze integreren in het authenticatieproces.
accounts.post('/accounts', async (req: Request<void, UserWithRole, IdOptional<User>>, res: Response<UserWithRole>) => {
const [user, session] = await createUser(req.body)
setSessionCookie(res, session)
res.send(user)
})login.post('/login', async (req: Request<void, UserWithRole, IdOptional<User>>, res: Response<UserWithRole | string>) => {
const user = await getUserByEmail(req.body.email)
const validCredentials = user && await verifyPassword(user.password, req.body.password)
if (!validCredentials) {
res.statusCode = 400
res.send('Invalid credentials')
return
}
const session = await startSession(user?.id as string)
setSessionCookie(res, session)
res.send(user!)
})Sessie uitlezen
We willen doorheen heen de applicatie weten of een gebruiker al dan niet ingelogd is, hiervoor schrijven we een nieuwe middleware functie.
Voordat we dit kunnen implementeren, moeten we een extra library installeren die de cookies uit een request uitleest. Hiervoor gebruiken we de cookie-parser bibliotheek.
Deze middleware functie moet dan natuurlijk ook in app.ts ingeladen worden.
import express from 'express'
import index from './routes/index.js'
import {corsMiddleware} from './middleware/cors.js'
import cookieParser from 'cookie-parser'
import {sessionManagement} from './middleware/sessionManagement.js'
const app = express()
const port = 3000
app.use(corsMiddleware)
app.use(cookieParser())
app.use(express.json())
app.use(index)
app.listen(port, () => {
console.log(`Express is listening at http://localhost:${port}`)
})Het interessant als we meer dan het sessionId ophalen in deze middleware functie. Met het sessionId op zich kunnen we niet veel doen, we hebben ook de bijhorende gebruikersgegevens nodig. Door het request object uit te breiden kunnen we deze gebruikersgegevens in alle volgende middleware functies en route handler uitlezen. We breiden de Request interface uit Express uit met een nieuwe property session.
De getSession functie waarmee een gebruiker opgehaald kan worden op basis van het sessionId is voorzien in de startbestanden en is hieronder enkel toegevoegd voor de duidelijkheid. Merk op dat er in deze functie eerst gecontroleerd wordt of het id wel gedefinieerd is, als deze controle niet toegevoegd is, is het mogelijk dat Prisma een foutmelding opgooit omdat het sessionId uiteraard undefined is als het cookie niet aanwezig is.
import {NextFunction, Request, Response} from 'express'
import {extendSession, getSession} from '../dal/sessions.js'
export async function sessionManagement(req: Request, res: Response, next: NextFunction) {
const sessionId = req.cookies.session
req.session = await getSession(sessionId)
next()
}export async function getSession(id: string | undefined): Promise<SessionWithUserAndRole | null> {
if (!id) {
return null
}
return prismaClient.session.findUnique({
where: {id},
include: {
user: {
include: {
role: true,
},
},
},
})
}Bovenstaande code werkt wel, maar genereert ook een TypeScript error (die genegeerd wordt tijdens het compilatieproces):
Om dit probleem op te lossen moeten we de definitie van de Request interface aanpassen. Dit doen we door middel van onderstaande code.
import {SessionWithUserAndRole} from './sessionTypes.js'
declare global {
namespace Express {
export interface Request {
session: SessionWithUserAndRole | null
}
}
}Sessie vernieuwen
Omdat de sessie vervalt na 24 uur moeten we de sessie vervangen als deze binnenkort vervalt. De perfecte plaats om dit te implementeren is natuurlijk opnieuw de middleware functie. We verversen de sessie hieronder als deze minder dan 12 uur geldig is.
Merk op dat het cookie ook opnieuw moeten instellen omdat hierin ook een expiration date zit die bijgewerkt moet worden.
De extendSession functie die hieronder gebruikt wordt, is beschikbaar in de startbestanden en is hieronder enkel toegevoegd voor de volledigheid.
export async function sessionManagement(req: Request, res: Response, next: NextFunction) {
const sessionId = req.cookies.session
req.session = await getSession(sessionId)
if (req.session) {
// Extend the session if it is close to expiring.
const delta = (req.session?.activeUntil.getTime() as number) - Date.now()
if (delta <= 1000 * 60 * 60 * 12) {
const activeUntil = new Date(Date.now() + 1000 * 60 * 60 * 24)
const session = await extendSession(sessionId, activeUntil)
setSessionCookie(res, session)
}
}
next()
}export async function extendSession(id: string, activeUntil: Date): Promise<Session> {
return prismaClient.session.update({
where: {id},
data: {
activeUntil,
},
})
}Tenslotte moet deze middleware functie dan opgeroepen worden in app.ts, natuurlijk moet dit nadat de cookie-parser ingeladen is.
import express from 'express'
import index from './routes/index.js'
import {corsMiddleware} from './middleware/cors.js'
import cookieParser from 'cookie-parser'
import {sessionManagement} from './middleware/sessionManagement.js'
const app = express()
const port = 3000
app.use(corsMiddleware)
app.use(cookieParser())
app.use(express.json())
app.use(sessionManagement)
app.use(index)
app.listen(port, () => {
console.log(`Express is listening at http://localhost:${port}`)
})Routes beveiligen
We hebben sessies geïmplementeerd, maar we doen er nog niets mee. Aangezien er verschillende routes zijn waarvoor we moeten controleren of de gebruiker ingelogd is, zullen we deze controle eveneens afzonderen in een nieuwe middleware functie.
Het is hier zeer belangrijk dat er een if-else gebruikt wordt, res.sendStatus beëindigd de functie niet. Dus als we de functie zouden herschrijven als if (!res.session) {res.sendStatus(401)} next(), is het mogelijk dat de status ingesteld wordt en dat de volgende middleware functie of route handler opgeroepen wordt. Dit zorgt dan voor errors omdat het niet mogelijk is om het response-object aan te passen als er al iets verzonden is naar de client.
import {NextFunction, Request, Response} from 'express'
export async function authenticatedRoute(req: Request, res: Response, next: NextFunction) {
if (req.session) {
next()
} else {
res.sendStatus(401)
}
}Middleware gebruiken
De voorbije lessen hebben we een middleware functie steeds gekoppeld aan een volledig pad, dit is nu echter niet mogelijk. Voor de POST /account route (account aanmaken) moet een gebruiker niet ingelogd zijn, maar voor de DELETE /account route moet de gebruiker wel ingelogd zijn. Het is niet mogelijk om een niet-ingelogde gebruiker te verwijderen omdat de handler voor de DELETE /account route het id van de gebruiker uitleest uit het request object, meer specifiek uit de session property die we hierboven gedefinieerd hebben.
Een middleware functie kan, behalve via de use methode, ook gekoppeld worden via de get, post, delete en put methodes. Elk van de eerder genoemde methodes accepteert een variabel aantal middleware functies en in het geval van de get, post, *delete$ en put methodes ook een route handler.
import {authenticatedRoute} from '../../middleware/authenticatedRoute.js'
accounts.delete('/accounts', authenticatedRoute, async (req: Request, res: Response) => {
// Nog te implementeren.
})
accounts.use('/accounts', authenticatedRoute, orders)logout.delete('/logout', authenticatedRoute, async (req: Request, res: Response) => {
// Nog te implementeren
})Sessie stoppen
Natuurlijk moeten we de sessie stopzetten als de gebruiker uitlogt of als de account verwijderd wordt. Stopzetten betekent in dit geval dat de activeUntil kolom ingesteld wordt op het huidige tijdstip en dat het cookie verwijderd moet worden. In het geval dat de account verwijderd wordt, moet enkel het cookie verwijderd worden omdat de sessies een cascade relatie gebruiken en mee verwijderd worden met de account. De stopSession methode is al aanwezig in de startbestanden, maar wordt hieronder herhaald voor de duidelijkheid.
const cookieName = 'session'
export function clearSessionCookie(res: Response): void {
res.clearCookie(cookieName)
}logout.delete('/logout', authenticatedRoute, async (req: Request, res: Response) => {
await stopSession(req.cookies.session)
clearSessionCookie(res)
res.sendStatus(200)
})accounts.delete('/accounts', authenticatedRoute, async (req: Request, res: Response) => {
await deleteUser(req.session?.userId as string)
clearSessionCookie(res)
res.sendStatus(200)
})export async function stopSession(id: string): Promise<void> {
prismaClient.session.update({
where: {id},
data: {
activeUntil: new Date(),
},
})
}Onderstaande video demonstreert dat de session tokens verwijderd worden.
Autorisatie
Hierboven hebben we authenticatie besproken, i.e. het identificeren van gebruikers. In het vervolg van de les bespreken we hoe we autorisatie kunnen implementeren, i.e. welke gebruikers welke rechten hebben.
Database schema
Het schema bevat reeds een tabel Role, hierin worden de verschillende rollen gedefinieerd. Voor dit voorbeeld zijn er twee rollen beschikbaar, admin en customer. Het enige verschil tussen de twee is dat administrator het recht heeft om producten toe te voegen aan de webshop.
model User {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @unique @db.VarChar(255)
password String
role Role @relation(fields: [roleId], references: [id])
roleId String @db.Uuid
sessions Session[]
Order Order[]
}
model Role {
id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @unique @db.VarChar(255)
User User[]
}Admin middleware
De methode die we in de authenticatedRoute middleware gebruikt hebben om de gebruikersinformatie op te halen bevat de rol van de gebruiker al. We moeten enkel een nieuwe middleware functie schrijven die controleert of deze rol admin is.
In de middleware functie gebruiken we de Roles enumeratie die al beschikbaar was in de startbestanden.
export async function adminRoute(req: Request, res: Response, next: NextFunction) {
if (req.session?.user?.role.name === Roles.admin) {
next()
} else {
res.sendStatus(403) // Forbidden
}
}export enum Roles {
customer = 'customer',
admin = 'admin'
}De nieuwe middleware functie moet tenslotte nog gekoppeld worden aan de POST /products route.
products.post('/products', adminRoute, async (_: Request, res: Response<Product>) => {
const product = await createProduct()
res.send(product)
})Voorbeeldcode
Uitgewerkt lesvoorbeeld met commentaar
Via de argon2-ffi library kan Argon2 gebruikt worden in Node. We gebruiken deze library niet in deze cursus omdat deze heel wat extra configuratie vereist. Alle stappen die op het GitHub repository van node-gyp staan moeten hiervoor uitgevoerd worden. ↩︎