Rezepte zum Entwickeln von Webanwendungen mit Node.js, Express.js und TypeScript – Teil 2

Keine Kommentare

Dieser Artikel und die Code-Beispiele bauen auf dem ersten Teil der Artikelserie auf. Hast du ihn bereits gelesen?

In diesem Artikel, dem zweiten Teil, geht es um Rezepte, die während des Betriebs und der Weiterentwicklung einer Node.js- und Express.js-Webanwendung interessant sind. Es werden Libraries vorgestellt, die dabei helfen, eine sichere Anwendung zu bauen. Außerdem wird einfaches Rate Limiting umgesetzt, um Request-Spamming vorzubeugen und kritische Schnittstellen zu schützen. Erweitertes Rate Limiting in verteilten Systemen wird ebenfalls diskutiert.
Der Einsatz von Feature Toggles bietet während der Auslieferung und Entwicklung von Software viele Vorteile, daher wird auch dieses Rezept vorgestellt. Als Dessert steht ein Rezept für die fachgerechte Konfiguration von Webanwendungen auf dem Plan, denn der sichere und flexible Umgang mit Secrets und anderen Variablen soll gelernt sein.

Das Repository, in dem die folgenden Rezepte demonstriert und zu einem Boilerplate-Projekt werden, kennst du ja bereits. Sobald alle Werkzeuge und Zutaten aus dem ersten Artikel bereitliegen, kann auch schon mit dem ersten Rezept gestartet werden.

Web-Sicherheit in Express.js-Anwendungen

IT-Sicherheit wird in der Softwareentwicklung immer noch nicht ernst genug genommen und sollte in Webanwendungen ein first-class citizen sein. In der heutigen, schnelllebigen IT ist es nicht untypisch, dass regelmäßige Threat Modelling Sessions, Penetration Tests und Dependency-Checks zu kurz kommen. Es fehlt oft das grundlegende Bewusstsein über die Wichtigkeit von sicherer Geschäftslogik und deren technischer Umsetzung.

Um auf Seiten der Entwicklung grobe Fehler zu vermeiden und ein paar grundlegende Security-Header korrekt zu konfigurieren, hilft die Node-Library Helmet. Sie beinhaltet 14 Middlewares zum Setzen von Security-Headern. Sie helfen dabei, grundlegende Sicherheitsprobleme einer Webawendung (siehe OWASP Top Ten Project) zu verhindern. Der Default-Export von Helmet ist eine Middleware, die die sieben allgemeingültigsten Middlewares aus der Sammlung zusammenfasst. Zusätzlich sollte man sich anschauen, ob der Einsatz der anderen Middlewares für die eigene Anwendung von Nutzen ist. So sieht es aus, wenn neben der Standard-Middleware noch die Referrer Policy, No Cache und Content Security Policy Middlewares genutzt werden:

private setupSecurityMiddlewares(server: Express) {
    server.use(hpp())
    server.use(helmet())
    server.use(helmet.referrerPolicy({ policy: 'same-origin' }))
    server.use(helmet.noCache())
    server.use(helmet.contentSecurityPolicy({
        directives: {
            defaultSrc: ["'self'"],
            styleSrc: ["'unsafe-inline'"],
            scriptSrc: ["'unsafe-inline'", "'self'"]
        }
    }))
}

Weitere sicherheitsrelevante Maßnahmen für Express.js-Anwendungen sind u. a. die Verwendung von TLS, Anti-CSRF (Cross Site Request Forgery) Tokens und durchdachte CORS (Cross-origin resource sharing) Konfigurationen. Empfehlenswerte Libraries, die dabei helfen, sind:

  • csurf – Middleware zur Implementierung von Anti-CSRF Tokens
  • cors – Middleware zur Konfiguration von Cross-origin resource sharing
  • hpp – Middleware zum Schutz vor HTTP Parameter Pollution (HPP) attacks

Die Libraries wurden auf Branch „06-web-security-and-rate-limiting“ im Beispiel-Projekt installiert und benutzt. Die Maßnahmen aus dem nächsten Abschnitt befinden sich ebenfalls auf dem Branch.

Rate Limiting in Webanwendungen mit Express.js

Um kritische API-Endpunkte der Anwendung rudimentär vor Request Hammering zu schützen, ist Rate Limiting hilfreich. Für Express hat sich die Library express-rate-limit aufgrund ihrer Einfachheit bewährt. Rate-Limiting findet damit standardmäßig auf Basis der IP-Adressen von Nutzern statt. Das heißt, dass, wenn ein Client mit gleichbleibender IP-Adresse das Rate-Limit erschöpft hat, weitere Anfragen seinerseits vom Server geblockt werden. Bis das Zeitfenster für diesen Client wieder Anfragen zulässt, antwortet der Service dem Client mit dem HTTP Status Code 429 “Too Many Requests”.

Der State über das Rate Limiting wird von der Middleware standardmäßig nur In-Memory gehalten. Wenn penible Genauigkeit der Limits wichtig ist, muss im Betrieb mehrerer Instanzen der Anwendung darauf geachtet werden, dass dieses Rate Limit pro Instanz ausgeschöpft werden kann. Laufen beispielsweise drei Instanzen und es sind 100 Anfragen pro Minute pro IP-Adresse erlaubt, ergibt das insgesamt ein Kontingent von 300 Anfragen pro Minute für einen Client.

Alle vom Load Balancer ansprechbare Instanzen machen für sich alleine Rate Limiting und haben den State darüber nur In-Memory, somit ist er nicht zwischen den Instanzen synchronisiert.

Alle Instanzen halten den State über das Rate Limiting nur im Arbeitsspeicher. Zählerstände laufen auseinander, irgendwann blockieren eventuell einzelne Instanzen den Client.

In einer hoch-elastischen Produktionsumgebung entstehen allerdings Ungenauigkeiten: Instanzen werden dauernd hoch- und heruntergefahren und verlieren den Rate Limiting State. In solchen Szenarien können mit der Library auch Redis, Memcached oder Mongo als Store verwendet werden, sodass die Instanzen ihren State über Rate Limiting teilen.
Falls wir spezielleres Verhalten beim Rate Limiting benötigen, bietet express-rate-limit noch weitere Optionen. Um Response-Zeiten der Anwendung zu verlängern, anstatt die Requests mit 429 “Too Many Requests” zu beantworten, kann express-slown-down genutzt werden. Reicht das noch nicht und wir wollen eventuell ein komplexeres API-Budget-Verhalten umsetzen oder andere Stores für das Teilen des States zwischen Instanzen, lohnt sich ein Blick auf rate-limiter-flexible.

Alle Instanzen der Express.js-Anwendung halten den State über Rate Limiting am selben Ort und sind daher auf dem selben Stand.

Alle Instanzen greifen beim Rate Limiting auf denselben und persistenten State zu, somit können die Instanzen die Anzahl der Requests des Clients deterministisch regulieren.

Auf Branch „06-web-security-and-rate-limiting“ wurden Rate Limiting und grundlegende Web-Sicherheit durch die vorgestellten Middlewares implementiert. Die Änderungen seit dem letzten Stand sehen wir übersichtlich im Pull Request #5.

Feature Toggles mit Express.js – Funktionalität per Schalter oder Kriterium ausrollen

Einfach ausgedrückt sind Feature Toggles (auch Feature Flags genannt) dynamische, konfigurierbare Schalter, die als Boolsche Ausdrücke im Code verwendet werden. Sie entscheiden darüber, ob Code ausgeführt oder übersprungen wird oder können darüber entscheiden, welcher Code ausgeführt wird. Diese mächtige, aber im Prinzip simple Technik erlaubt es mit minimalem Eingriff in den Code, das Verhalten des Systems bewusst zu ändern. Es gibt viele Szenarien, in denen sie praktisch ist. Beispielsweise können Teams Trunk-Based Development dadurch praktizieren, dass Code eines Features früh in den Haupt-Branch und somit in die Produktionsumgebung kommt. Mit einem Feature Toggle wird dieses sich noch in Arbeit befindliche Feature erst dann aktiviert, wenn es fertiggestellt ist. Ebenfalls beim Verproben von neuen Features oder Technologien helfen Feature Toggles dabei, Code nur für einen Teil der Anwendungsbenutzer zu aktivieren.
Es gibt weitere interessante Einsatzgebiete für Feature Toggles, wie auch damit hergehende Herausforderungen, die von Pete Hodgson ausführlich in einem Artikel beschrieben werden.

In der Welt von Node.js ist die Library fflip für die leichtgewichtige und nicht-persistente Definition von Features und den Kriterien, unter denen sie ausgeführt werden sollen, geeignet. Der folgende Code zeigt ein minimales Beispiel:

import { Express, NextFunction, Response, Request } from 'express'
import * as fflip from 'fflip'
import * as FFlipExpressIntegration from 'fflip-express'

export const features: fflip.Feature[] = [
    { id: ‘CLOSED_BETA’,  criteria: { isPaidUser: true, shareOfUsers: 0.5 } },
    { id: ‘WITH_CAT_STATISTICS’,  enabled: true }
]
const criteria: fflip.Criteria[] = [
    {
        id: 'isPaidUser',
        check: (user: any, needsToBePaid: boolean) => user && user.isPaid === needsToBePaid
    },
    {
        id: 'shareOfUsers',
        check: (user: any, share: number) => user && user.id % 100 < share * 100
    }
]
export const applyFeatureToggles = (server: Express) => {
    fflip.config({ criteria, features })
    const fflipExpressIntegration = new FFlipExpressIntegration(fflip, {
        cookieName: 'fflip',
        manualRoutePath: '/api/toggles/local/:name/:action'
    })

    server.use(fflipExpressIntegration.middleware)
    server.use((req: Request, _: Response, next: NextFunction) => {
        req.fflip.setForUser(req.user)
        next()
    })
}

Für Express.js gibt es ebenfalls fflip-express. Damit kann eine Middleware erstellt werden, die global oder nur in spezielle Request-Mappings eingehängt wird. Die Middleware stellt dann für die Implementierung der Request-Verarbeitung die Methode req.has(featureName) zur Verfügung. Mithilfe dieser kann im Code der Zustand eines Features abgefragt werden. Es sollte darauf geachtet werden, dass Feature Toggles ihren Status während des Lebenszyklus der laufenden Anwendung ändern können sollten. Bietet ein neues Feature also einen REST-Endpunkt an, sollte der initial deaktivierte Toggle also nicht die Instanziierung des Request Mappings (z.B. server.get('/api/cat/status', catEndpoints.catStatus)) ausklammern, sondern die in der Funktion ausgeführte Logik. Der Grund dafür ist, dass die Instanziierung des Request Mappings nur einmal ausgeführt wird. Das Feature könnte somit nicht ohne einen Neustart aktiviert werden.

public getCatStatistics = async (req: Request, res: Response, next: NextFunction) => {
    try {
        if (req.fflip.has(FeatureToggles.WITH_CAT_STATISTICS)) {
            res.json(req.services.catService.getCatsStatistics())
        } else {
            res.sendStatus(HttpStatus.NOT_FOUND)
        }
    } catch (err) {
        next(err)
    }
}

„Binäre Features“ wie withLandingPage aus dem vorletzten Code-Beispiel sind ohne Zutun nicht während der Laufzeit veränderbar. Zur Aktivierung des Features muss der Code geändert und neu ausgerollt werden. Möchte man Features während der Laufzeit der Anwendung aktivieren oder deaktivieren, sollte der State der Toggles in der Datenbank persistiert werden. Den State der Toggles nur im Speicher der Anwendung zu halten, ist über Neustarts hinweg flüchtig. Spätestens wenn mehrere Instanzen parallel betrieben werden und Traffic „zufällig“ auf Instanzen geleitet wird, disqualifiziert sich Toggle-State im Speicher, da der Speicher der Instanzen nicht synchronisiert wird. Ein einfacher Key-Value Store oder eine Datenbank sollte genutzt werden, um den Toggle-State über Neustarts und Deployments hinweg persistent zu halten. Leider bietet uns fflip dieses Feature nicht. Sobald man spezielle Anforderungen (wie die Art der Persistenz des States) hat, kann dies selbst implementiert werden. Einfacher wäre es jedoch, einen Blick auf Unleash zu werfen. Unleash bietet einen Open-Source-Server sowie Client-Schnittstellen für Feature Toggles an. Dazu hostet man den Unleash-Server inklusive PostgreSQL Datenbank und wählt die passende Client Library, in unserem Fall unleash-client-node, zur Kommunikation mit dem Server. Diese Variante bietet zwar alles, was benötigt wird, kann aber aktuell nur mit PostgreSQL arbeiten. Mein Kollege Mitchell Herrijgers hat einen ausführlichen Artikel über die Nutzung von Feature Toggles in Microservice-Umgebungen mit Unleash verfasst, der detaillierter auf dessen Ansatz eingeht.

Für dieses Kochbuch belassen wir es bezüglich Feature Toggles bei der Nutzung von fflip. Auf Branch 07-fflip-feature-toggles wurden beispielhafte Änderungen implementiert, Änderungen im Vergleich zum letzten Branch werden wie immer im Pull Request sichtbar.

Konfiguration einer Webanwendung in Node.js und Express.js

Konfigurationsmanagement hat in der IT eine eher überwürzte Bedeutung. Ich möchte es nicht verkomplizieren, sondern aufzeigen, wie viel einfacher das Thema von Seiten der Webentwicklung angegangen werden kann. Die Methodologie “The Twelve-Factor App” bietet zum Thema Konfiguration eine knackige Erklärung. Im Wesentlichen wird vorgeschlagen, Konfiguration, die sich über unterschiedliche Umgebungen („Deploys“) hinweg ändert, über Umgebungsvariablen zu kontrollieren. Konfiguration sollte strikt vom Code separiert werden. Der Grund dafür ist, dass Konfiguration sich über unterschiedliche Umgebungen (wie lokale/Development-Umgebung, Staging, Produktion) hinweg ändert – Code jedoch nicht.

Node.js greift über process.env auf Umgebungsvariablen zu. Die ENVs werden meist von der Betriebsumgebung gesetzt, z.B. durch die Deployment-Mechanismen von Docker, Kubernetes, Nomad und co.

Node.js greift über process.env auf Umgebungsvariablen zu. Die ENVs werden meist von der Betriebsumgebung gesetzt, z. B. durch die Deployment-Mechanismen von Docker, Kubernetes, Nomad und co. Umgebungsvariablen werden pro „Deploy“ vom Entwicklungsteam kontrolliert.

Umgebungsvariablen (Environment Variables) eignen sich als Medium dafür ideal, da sie Sprach- und Betriebssystem-agnostisch gesetzt und ausgelesen werden können. Auch in unserem Node-Express-Stack versalzen sie uns nicht die Suppe. Das globale Objekt process.env enthält alle Umgebungsvariablen. Wir können bspw. eine Klasse bauen, die über statische Methoden alle Konfigurationen aus der Umgebung bereitstellt:

export class Environment {
    public static isLocal(): boolean {
        return Environment.getStage() === 'local'
    }
 
    public static isStaging(): boolean {
        return Environment.getStage() === 'staging'
    }
 
    public static isProd(): boolean {
        return Environment.getStage() === 'prod'
    }
 
    public static getStage(): string {
        return process.env.STAGE || 'local'
    }
 
    public static getPort(): number {
        return parseInt(process.env.PORT || '8000')
    }
}

Fallback-Werte zu definieren ergibt Sinn, wenn es Umgebungen (z. B. während der lokalen Entwicklung) gibt, in denen die Umgebungsvariablen nicht vorliegen. Es sollte darauf geachtet werden, keine echten Secrets (Usernames, Passwörter, Token, Zertifikate etc) als Fallback-Werte zu definieren.
Um die einfache Nutzung von Konfigurationen aus der Umgebung zu veranschaulichen, wurden auf Branch 08–environment-configuration einige Neuerungen implementiert (Vergleich zum vorherigen Schritt im Pull Request).

Abschließender Blick in die Buchstabensuppe

Ich hoffe, dass auch der zweite Artikel dieser Serie über Node, Express & TypeScript dein Repertoire an Rezepten und Tools bereichern konnte. Mithilfe der vorgestellten Kniffe lässt sich sicherere und einfacher zu wartende Software ausliefern. Ausgeprägtes Wissen über IT-Sicherheit, Feature Toggles und ein sauberer Umgang mit Konfiguration sind auch in jedem anderen Stack von großem Nutzen.
Gemäß dem Sprichwort „Ein guter Koch muss kosten“ lehrt uns das Node-Ökosystem wieder, dass die Wahl der richtigen Libraries Gold wert ist. Express oder Loopback? fflip oder Unleash? Das Angebot ist groß und es muss oft vorsichtig abgewägt werden, mit welcher Wahl man ein Probleme löst.

Kochrezepte mit der Buchstabensuppe voller Node.js, Express.js und TypeScript

In der Buchstabensuppe fanden sich einfach keine „N“ und „C“ Buchstaben. Ähnlich wie manchmal im Node-Ökosystem notwendig, war deshalb Pragmatismus gefragt.

Im nächsten Artikel geht es mit betriebsrelevanten Themen rund um Node.js und Express.js weiter. Dazu gehören Clustering, Monitoring und Containerisierung mit Docker. Gibt es Themen bezüglich dieses Stacks, die dich außerdem noch interessieren? Lass es mich gerne in den Kommentaren wissen.

Jonas Verhoelen

Jonas entwickelt leidenschaftlich gerne Software in agilen, crossfunktionalen Teams. Am liebsten begleitet er Web-Anwendungen ganzheitlich von der Idee bis zur Inbetriebnahme und darüber hinaus. Ursprünglich von der Java-Welt geprägt, entwickelt er aktuell bevorzugt mit React, Node.js und TypeScript. Zudem teilt er sein Fachwissen rund um Distributed Ledger Technologies und IT Security mit Kunden und der Community. An seinem Arbeitsumfeld schätzt er Selbstorganisation, Vertrauen, Transparenz und den Stellenwert von T-shaped skills.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.