Node.js mit TSOA und Authentifizierung - (Teil 5) Error-Middleware für standardkonforme Antworten
Nachdem wir in der Authentifizierungsmiddleware erfolgreich Fehler behandelt haben, werden wir eine zusätzliche Middleware einführen, die die Ausgabe standardkonform handhabt. Ich habe das ähnlich für „InvalidParameter“ bereits ausgearbeitet und wende es auf unseren konkreten Fall an. Auf geht’s!
In diesem Artikel beschäftigen wir uns mit der Fehlersituation bei der Header-Validierung. Das Ziel ist, ein RFC-konformes ProblemDocument zurückzugeben, wenn während der Ausführung ein Fehler auftritt. Wir übersetzen heute den „Unauthenticated-Error“ in eine „application/problem+json“-Antwort. Bevor wir anfangen, empfiehlt es sich, die Vorgänger der Artikelserie zu lesen:
- Teil 1: Die Zielsetzung
- Teil 2: Das Basis-Setup
- Teil 3: Ein JWT erzeugen
- Teil 4: Header-Validierung in der AuthMiddleware
RFC-konforme Antwort generieren
Wenn wir die Applikation im aktuellen Zustand ausführen und im Browser aufrufen (also ohne Authorization-Header), erhalten wir einen Statuscode 401 mit Content-Type text/html. Wünschenswert wäre eine JSON-Antwort.
Ich habe das Vorgehen dafür in einem vergangenen Artikel für InvalidParams mit TSOA beschrieben: Node.js und RFC-konforme Fehlermeldungen. Wir wenden das jetzt auf den hier definierten „Unauthenticated-Error“ an. Voraussetzung hierfür ist die Installation zweier NPM-Pakete.
npm install http-problem-details http-problem-details-mapper
Mithilfe der beiden Pakete gehen wir wie folgt vor:
- Wir benötigen einen Mapper unserer Fehlerklasse in ein ProblemDocument aus dem Paket http-problem-details
- Wir definieren eine MappingStrategy auf Basis des Pakets http-problem-details-mapper.
- Wir erstellen eine Express-Error-Middleware, die die Strategie und das Mapping anwendet.
- Wir hängen die Middleware in unsere Express-Applikation.
1. Das Fehlerklassen-Mapping
Wir ergänzen die Error-Klasse um einen Mapper.
export class UnauthenticatedMapper extends ErrorMapper {
constructor() {
super(UnauthenticatedError)
}
mapError(error: UnauthenticatedError) {
return new ProblemDocument({
status: error.statusCode,
title: 'Bad request',
type: 'unauthenticated',
detail: error.detail,
})
}
}
2. Die Mapping-Strategie definieren
Hierzu bedienen wir uns der installierten NPM-Pakete und registrieren unser Mapping aus Schritt 1.
import { DefaultMappingStrategy, MapperRegistry } from 'http-problem-details-mapper'
import { UnauthenticatedMapper } from '@/errors/UnauthenticatedError'
const mapperRegistry = new MapperRegistry()
.registerMapper(new UnauthenticatedMapper())
const ErrorMappingStrategy = new DefaultMappingStrategy(mapperRegistry)
3. Die Express-Error-Middleware
Die Error-Middleware muss als Letztes in die Express-Applikation eingebunden werden. Wenn sich der Fehler anhand der Strategie aus Schritt 2 übersetzen lässt, passen wir die Antwort an, ansonsten ignorieren wir ihn.
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'
import { IMappingStrategy } from 'http-problem-details-mapper'
interface HttpProblemResponseOptions {
strategy: IMappingStrategy
}
function HttpProblemResponse(options: HttpProblemResponseOptions): ErrorRequestHandler {
const { strategy } = options
return function HttpProblemDetailsMiddleware(
error: Error,
_request: Request,
response: Response,
next: NextFunction,
): void {
const problem = strategy.map(error)
// in case we cannot map the error, we will not handle it
if (!problem) {
next(error)
return
}
response.statusCode = problem.status
response.setHeader('Content-Type', 'application/problem+json')
response.json(problem)
}
}
export default HttpProblemResponse
4. Einbindung in Express
Als letzter Schritt bleibt die Einbindung in Express. Dazu nehmen wir das Mapping aus Schritt 2 und geben es als Parameter an die Middleware aus Schritt 3 weiter. Damit sieht unsere Express-Applikation wie folgt aus:
import express from 'express'
import { RegisterRoutes } from '@/routes'
import { DefaultMappingStrategy, MapperRegistry } from 'http-problem-details-mapper'
import HttpProblemResponse from '@/errors/HttpProblemResponse'
import { UnauthenticatedMapper } from '@/errors/UnauthenticatedError'
const PORT = process.env.PORT || 4000
const mapperRegistry = new MapperRegistry()
.registerMapper(new UnauthenticatedMapper())
const ErrorMappingStrategy = new DefaultMappingStrategy(mapperRegistry)
const app = express()
RegisterRoutes(app)
// The RFC 7807 error middleware has to be the last app.use() to work as intended
// It will map the thrown errors to the RFC applying responses
app.use(HttpProblemResponse({ strategy: ErrorMappingStrategy }))
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`)
})
Fazit
Die aktuelle Applikation funktioniert im Fehlerfall vorbildlich. Bei einem Aufruf ohne Authorization-Header erhalten wir eine RFC-konforme JSON-Antwort. Ein testweiser Aufruf:
liefert uns wie erwartet eine passende Antwort:
{
"type": "unauthenticated",
"title": "Bad request",
"detail": "No authorization header provided",
"status": 401
}
Damit ist der Weg frei, um uns um das Token und dessen Überprüfung zu kümmern. Das Herzstück der Implementierung findest du in Teil 6: Tokenvalidierung.