JavaScript genießt mit Node.js auf Backend-Seite steigende Beliebtheit. Mit Nest gibt es nun ein Framework, das das Entwickeln von testbaren, skalierbaren und wartbaren Node.js-Anwendungen ermöglicht. Der Artikel Einführung in Nest gibt bereits einen Überblick über das Framework. In diesem Artikel schauen wir uns an, wie man mit Guards den Zugriff auf Ressourcen in einer Nest-Anwendung schützt.

Ein neues Projekt starten

Nest stellt eine Command Line Interface bereit, welches für das Erstellen eines neuen Projekt verwendet werden kann. Alternativ kann ein Beispiel-Projekt von GitHub geklont werden. Wie dies geht, ist im bereits erwähnten Artikel erklärt.

Guards für Controller

Die Schnittstellen in Nest, welche Clients ansprechen, sind in Controllern beschrieben. Schnittstellen sind standardmäßig für jeden Client erreichbar. Um den Zugriff auf vertrauchliche Daten zu schützen, setzt man Guards ein.

Im folgenden Beispiel sollen nur autorisierte Nutzer Zugriff auf den Admin-Bereich haben. Hierzu erstellen wir einen AdminGuard. Die CLI hilft beim Anlegen des Guards:

$ nest g guard Admin
CREATE /src/admin.guard.spec.ts (160 bytes)
CREATE /src/admin.guard.ts (299 bytes)

Der Guard schützt alle Endpoints unter dem Controller AdminController. Anschließend initialisieren wir eine Methode mit dem Decorator @Get(). Um mit dem Guard alle Schnittstellen in dem Controller zu schützen, annotieren wir die Klasse mit dem Decorator @UseGuards():

// AdminController

@Controller('admin')
@UseGuards(AdminGuard)
export class AdminController {

  @Get()
  get(): string {
    return 'Admin Section';
  }

}

Ein Guard, welcher über die CLI angelegt wird, lässt zunächst alle Requests passieren:

export class AdminGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

Im Guard soll die Rolle des Nutzers geprüft werden und nur Nutzer mit der Rolle ‘admin’ sollen Zugriff auf die Ressource haben. Die Rolle wird im Header übertragen:

export class AdminGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();
    if (request.headers['role'] === 'admin') {
      console.log('Admin section');
      return true;
    } else {
      console.log('Admin access denied');
      return false;
    }
  }
}

Die Schnittstelle testen wir mit Postman:

Requests mit der Rolle 'admin' haben Zugriff.
Requests mit der Rolle ‘admin’ haben Zugriff.

Guards für Methoden

Der implementierte Guard schützt nun alle Endpoints unter dem Pfad admin.

Als nächstes schauen wir uns an, wie man innerhalb eines Controllers eine bestimmte Schnittstelle schützt.

Hierzu erstellen wir einen weiteren Guard namens OwnerGuard und eine weitere Methode im AdminController, welche von dem OwnerGuard geschützt wird:

@Post('createUser')
@UseGuards(OwnerGuard)
post(@Body() body: any): string {
  return 'User created';
}

Die neue Schnittstelle verarbeitet nur POST-Requests und ist unter dem Pfad /admin/createUser erreichbar.

Der OwnerGuard ist vergleichbar zum AdminGuard aufgebaut und lässt Nutzer mit der Rolle ‘owner’ passieren:

export class OwnerGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();
    if (request.headers['role'] === 'owner') {
      console.log('Owner access granted');
      return true;
    } else {
      console.log('Owner access denied');
      return false;
    }
  }
}

Damit alle anderen Schnittstellen im AdminController mit der Rolle ‘owner’ erreichbar sind, muss der AdminGuard erweitert werden:

if (request.headers['role'] === 'admin' || request.headers['role'] === 'owner') {
    ...
}

Wir testen die Schnittstelle mit Postman und sehen, dass Nutzer mit der Rolle ‘admin’ keinen Zugriff auf createUser haben:

Für das Anlegen eines User ist die Rolle 'admin' nicht ausreichend.
Für das Anlegen eines User ist die Rolle ‘admin’ nicht ausreichend.

Während Nutzern mit der Rolle ‘owner’ Zugriff gewährt wird:

Mit der richtigen Rolle lässt sich ein User anlegen.
Mit der richtigen Rolle lässt sich ein User anlegen.

Global Guard

Nun kann es unübersichtlich werden, wenn bei vielen Controllern jeder einzelne mit dem Guard annotiert wird. Will man die ganze Anwendung mit einem Guard sichern, so gibt es den Global Guard.

Um alle Schnittstellen an einer zentralen Stelle zu schützen, stellt Nest die Methode useGlobalGuards bereit. Sie wird in der main.ts wie folgt verwendet:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}

Der AuthGuard checkt, ob eine Rolle existiert:

export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request: Request = context.switchToHttp().getRequest();
    console.log(request.headers['role']);
    if (request.headers['role'] !== undefined) {
      console.log('Logged in');
      return true;
    } else {
      console.log('Not logged in');
      return false;
    }
  }
}

Fazit

Guards sind eine einfach zu implementierende Art und Weise, um wichtige Ressourcen auf Backend-Seite zu schützen. Sie sind sehr ähnlich aufgebaut wie Guards in Angular, was Angular-Entwicklern den Einstieg in Nest vereinfacht. Um die Guards zu demonstrieren, habe ich das Codebeispiel aus dem ersten Artikel erweitert. Der Code ist auf GitHub einsehbar.