In der dynamischen Programmiersprache JavaScript können wir mit einer Vielzahl von unerwarteten oder unvollständigen Daten konfrontiert werden. Diese Daten können von externen Quellen wie Benutzereingaben, API-Antworten oder Cookies stammen. Insbesondere müssen wir darauf vorbereitet sein, mit Variablen umzugehen, die null oder undefined sein können. TypeScript, als eine Erweiterung von JavaScript, bietet eine Reihe von Operatoren und Mechanismen, um mit solchen Unsicherheiten sicher und effektiv umzugehen.

JavaScript ist eine dynamisch typisierte Sprache, was bedeutet, dass der Typ einer Variablen zur Laufzeit ermittelt wird. Der JavaScript-Interpreter weiß also nicht im Voraus, welcher Typ einer Variablen zugewiesen ist. Dies führt dazu, dass der Typ zur Laufzeit deduziert werden muss. Grundlegende Typen wie boolean, number, string oder array sind dabei relativ einfach zu handhaben. Komplexe Typen, die oft als JSON-Objekte auftreten, können jedoch auch null oder undefined sein. Es gibt grundlegende Unterschiede zwischen diesen beiden Konzepten: undefined repräsentiert eine Variable, die weder deklariert noch initialisiert wurde, während null eine explizite Abwesenheit eines Wertes in einer deklarierten Variablen bedeutet.

In statisch typisierten Programmiersprachen wie Java oder C# gibt es das Konzept von undefined nicht. Dort haben einfache Datentypen wie Zahlen oder Strings Standardwerte wie 0 oder eine leere Zeichenkette. Komplexe Typen können jedoch null sein. Die Einführung des null-Verweises in Programmiersprachen gilt als einer der größten Fehler in der Informatikgeschichte, ein "Milliarden-Dollar-Fehler", wie der Erfinder Tony Hoare es selbst bezeichnete.

TypeScript versucht, die Vorteile einer statischen Typisierung in das dynamisch typisierte JavaScript zu integrieren, indem es zusätzliche Typen wie null, undefined, any und never einführt. Diese Typen helfen dabei, die komplexen Semantiken von JavaScript zu handhaben und ermöglichen eine sicherere und weniger fehleranfällige Programmierung.

Ein wichtiger Aspekt in TypeScript ist die Unterscheidung zwischen null und undefined. Während beide Werte in JavaScript oft als "falsy" gelten, behandelt TypeScript diese beiden Zustände unterschiedlich. Zum Beispiel ist der Typ string | null ein anderer als string | undefined, und auch string | undefined | null ist wieder ein separater Typ. Diese Nuancen werden vor allem bei der Arbeit mit strikten TypeScript-Regeln und Frameworks wie Angular deutlich.

Ein weiteres wichtiges Konzept in TypeScript ist die Unterscheidung zwischen den Vergleichsoperatoren == und ===. Der doppelte Gleichheitsoperator == prüft nur, ob der Wert einer Variablen definiert und nicht null ist. Der strikte Vergleichsoperator === prüft hingegen, ob der Wert exakt gleich und nicht null oder undefined ist. Diese Feinheiten beeinflussen, wie wir sicherstellen können, dass eine Variable tatsächlich einen gültigen Wert enthält, ohne unerwartete Fehler zu verursachen.

Wenn wir mit Variablen arbeiten, die möglicherweise null oder undefined sein können, müssen wir sorgfältig prüfen, ob die Variable "truthy" ist. Ein "truthy" Wert ist eine Variable, die deklariert wurde und nicht null oder undefined ist. Dies ist besonders wichtig, wenn wir mit Daten arbeiten, die von externen Quellen stammen, wie etwa Benutzereingaben oder API-Antworten. Zum Beispiel könnte der folgende Code eine einfache Prüfung auf den Wahrheitswert einer Variablen durchführen:

typescript
const foo: string = undefined;
if (foo) { console.log('truthy'); } else { console.log('falsy'); }

In diesem Beispiel wird die Variable foo als "falsy" betrachtet, wenn sie null, undefined oder eine leere Zeichenkette ist. Wenn wir auf diese Weise mit Variablen umgehen, können wir sicherstellen, dass sie die erwarteten Werte enthalten und keine unerwarteten Fehler im Code auftreten.

Für eine kompakte und lesbare Darstellung von Bedingungen kann der bedingte Operator (? :), auch ternärer Operator genannt, verwendet werden. Dieser Operator erlaubt es, Bedingungen in einer einzigen Zeile zu überprüfen und auszuwerten:

typescript
const foo: string = undefined;
console.log(foo ? 'truthy' : 'falsy');

Dieser Operator ist besonders nützlich, wenn der Rückgabewert von Bedingungen direkt verwendet werden soll, was die Lesbarkeit des Codes erheblich verbessert. In Fällen, in denen eine Standardwertverwendung erforderlich ist, kann der nullish coalescing Operator (??) verwendet werden. Dieser Operator stellt sicher, dass nur null oder undefined durch einen Standardwert ersetzt wird, jedoch nicht durch andere falsy Werte wie eine leere Zeichenkette.

typescript
const foo: string = undefined;
console.log(foo ?? 'bar');

In diesem Fall wird foo nur dann durch 'bar' ersetzt, wenn foo tatsächlich null oder undefined ist. Ein leerer String würde nicht zum Standardwert führen.

Für den Fall, dass ein komplexes Objekt oder eine tief verschachtelte Eigenschaft geprüft werden muss, bietet TypeScript den optionalen Verkettungsoperator (?.). Dieser Operator prüft, ob eine Variable oder Eigenschaft existiert, bevor auf eine darunterliegende Eigenschaft oder Methode zugegriffen wird. Dies verhindert, dass bei fehlenden oder null-Werten Fehler auftreten.

typescript
const user = {
name: { first: 'Doguhan', middle: null, last: 'Uluca' } }; console.log(user?.name?.middle ?? '');

Der oben stehende Code stellt sicher, dass auf die Eigenschaft middle nur zugegriffen wird, wenn sie existiert, andernfalls wird der Standardwert '' (eine leere Zeichenkette) zurückgegeben.

Durch den Einsatz dieser Operatoren und Techniken in TypeScript können wir den Umgang mit unsicherem oder unvollständigem Datenmaterial effektiv gestalten und gleichzeitig die Robustheit und Lesbarkeit des Codes verbessern. Besonders in modernen Webanwendungen, die häufig mit externen Datenquellen kommunizieren, sind solche Sicherheitsmaßnahmen unverzichtbar.

Wie implementiert man eine benutzerdefinierte Authentifizierung für REST und GraphQL in einer modernen Webanwendung?

Die Implementierung von Authentifizierung in Webanwendungen ist ein grundlegendes Thema, das sowohl für Entwickler als auch für Sicherheitsexperten von zentraler Bedeutung ist. In modernen Full-Stack-Anwendungen wird die Authentifizierung und Autorisierung häufig über RESTful APIs oder GraphQL-Resolver gehandhabt. Ein gutes Verständnis davon, wie diese Technologien zusammenarbeiten, ist entscheidend für den erfolgreichen Betrieb einer Anwendung, die sowohl Sicherheit als auch Benutzerfreundlichkeit gewährleistet.

Die zentrale Funktion in der hier beschriebenen Architektur ist die Authentifizierungsmiddleware, die in einer Express.js-Anwendung implementiert wird. Diese Middleware überprüft, ob der Benutzer authentifiziert ist, indem sie ein JWT (JSON Web Token) aus dem Header der Anfrage decodiert und den Benutzer in der Datenbank findet. Falls der Benutzer existiert und die erforderlichen Berechtigungen erfüllt, wird der Benutzer zur Antwort hinzugefügt und die Anfrage wird fortgesetzt. Andernfalls wird eine Fehlermeldung zurückgegeben. Diese Logik wird in der authenticate-Methode der Express.js-App implementiert.

Die authenticate-Methode übernimmt zwei wesentliche Aufgaben: Zuerst wird überprüft, ob der Benutzer über ein gültiges Authentifizierungstoken verfügt. Ist dies der Fall, wird zusätzlich geprüft, ob der Benutzer die erforderliche Rolle hat, um auf die angeforderte Ressource zuzugreifen. Ist alles korrekt, wird der Benutzer an die Antwort angehängt, und der Kontrollfluss wird an die nächste Middleware übergeben. Andernfalls wird eine Fehlerantwort mit dem Status 401 (unauthenticated) oder 403 (forbidden) zurückgegeben, je nachdem, ob der Benutzer fehlt oder nicht die notwendigen Berechtigungen hat.

In der Praxis wird diese Methode in einer Vielzahl von Endpunkten verwendet, wie im Beispiel der /me-Route. Hier wird die Middleware aufgerufen, bevor die Benutzerdaten zurückgegeben werden. Dies sorgt für eine bequeme und zentrale Möglichkeit, die Authentifizierung zu verwalten und sicherzustellen, dass sensible Informationen nur für berechtigte Benutzer zugänglich sind.

Eine ähnliche Architektur wird in GraphQL verwendet, jedoch mit einigen wichtigen Unterschieden. In GraphQL wird Authentifizierung und Autorisierung meist getrennt behandelt. Ein Vorteil von GraphQL liegt darin, dass die Abfragen und Mutationen von einem zentralen Resolver verarbeitet werden. Hier wird ein spezieller Mechanismus genutzt, um die Authentifizierung zu überprüfen und sicherzustellen, dass nur berechtigte Benutzer auf bestimmte Daten zugreifen können. Bei der GraphQL-Implementierung kann man bestimmte Ausnahmen definieren, wie beispielsweise für die Login-Mutation oder die Introspektionsabfragen, die für die Funktionsweise der API erforderlich sind.

Die Lösung besteht darin, eine Ausnahme für bestimmte GraphQL-Operationen zu schaffen, die keine Authentifizierung erfordern. In den meisten anderen Fällen wird jedoch die authenticate-Methode aufgerufen, um sicherzustellen, dass alle Benutzer, die auf geschützte Daten zugreifen wollen, auch wirklich authentifiziert sind. Dies stellt sicher, dass der Schutz der Daten zentral und konsistent über alle API-Endpunkte hinweg gewährleistet ist.

Darüber hinaus wird eine benutzerdefinierte Authentifizierungslogik für REST und GraphQL im Frontend bereitgestellt. Für RESTful APIs wird der HTTP-Client verwendet, um sich mit der Authentifizierungs-API zu verbinden. Die login-Methode sendet Benutzeranmeldedaten an den Server, und das erhaltene JWT wird im Frontend gespeichert. Der Benutzer kann dann durch Aufrufe der getCurrentUser-Methode authentifiziert werden, die Informationen über den aktuellen Benutzer abruft. Es ist wichtig, bei der Kommunikation über das Internet stets HTTPS zu verwenden, da sonst Benutzerinformationen durch unsichere Verbindungen abgefangen werden können.

Für GraphQL wird die Authentifizierung über den Apollo Client abgewickelt. Hierbei wird eine Mutation für den Login sowie eine Abfrage für den aktuellen Benutzer verwendet. Diese Struktur ermöglicht eine nahtlose Integration von Authentifizierung und Benutzerabfragen in GraphQL, ohne den Fluss der Anwendung zu stören. Der Einsatz des Apollo-Clients und von GraphQL erfordert allerdings eine präzise Handhabung der Authentifizierung, da jeder Resolver individuell geprüft werden muss, ob der Benutzer berechtigt ist, auf die angeforderten Daten zuzugreifen.

Ein weiteres Schlüsselelement dieser Architektur ist die Authentifizierungs-Factory, die dafür sorgt, dass der richtige Authentifizierungsdienst für die jeweilige Authentifizierungsstrategie ausgewählt wird. In der Factory wird festgelegt, ob der Dienst für RESTful APIs oder GraphQL verwendet wird, was eine flexible und erweiterbare Lösung für unterschiedliche Authentifizierungsstrategien bietet. Diese Möglichkeit, zwischen verschiedenen Authentifizierungsmodellen zu wechseln, macht die Architektur besonders robust und anpassbar.

Es ist von entscheidender Bedeutung, dass alle Authentifizierungsdaten und -token sicher gespeichert und behandelt werden, um Sicherheitslücken zu vermeiden. Insbesondere bei der Verwendung von JWTs sollte darauf geachtet werden, dass diese Tokens niemals unverschlüsselt über unsichere Kanäle gesendet werden. Ein weiterer wichtiger Aspekt ist die ordnungsgemäße Handhabung von Benutzerrollen und Berechtigungen. Eine fehlerhafte Implementierung dieser Logik könnte dazu führen, dass unberechtigte Benutzer auf sensible Daten zugreifen, was schwerwiegende Sicherheitsprobleme verursachen kann.

Abschließend lässt sich sagen, dass die Authentifizierung in modernen Webanwendungen eine anspruchsvolle, aber essentielle Aufgabe ist. Sie erfordert sorgfältige Planung und Implementierung, um sowohl die Sicherheit als auch die Benutzerfreundlichkeit zu gewährleisten. Die hier vorgestellten Lösungen bieten einen umfassenden Ansatz zur Authentifizierung, der REST und GraphQL nahtlos integriert und den Entwicklern eine hohe Flexibilität bei der Wahl ihrer Authentifizierungsstrategie ermöglicht.