Angular Auth Guards - Ein Roboter mit einem Schild als Körper steht als Türsteher vor einem Club

Angular Auth Guards: Deine Türsteher für sichere Routen

Angular Auth Guards sorgen dafür, dass nur berechtigte Nutzer:innen bestimmte Bereiche deiner Anwendung sehen können. In diesem Artikel erfährst du, was sie sind, wie du sie einsetzt und bekommst praktische Beispiele an die Hand – sowohl für den klassischen Ansatz mit Klassen als auch für die moderne funktionale Implementierung.

Was sind Angular Auth Guards und wozu brauchst du sie?

Stell dir Auth Guards wie einen Türsteher für deine Angular-Routen vor. Sie entscheiden, ob ein:e Nutzer:in Zugang zu einer bestimmten Route oder einem Bereich deiner App erhält. Das ist besonders nützlich, wenn du Teile deiner Anwendung schützen möchtest, etwa einen Admin-Bereich oder persönliche Nutzerprofile.

Auth Guards in Angular sind im Grunde genommen Dienste, die das CanActivate, CanActivateChild oder CanLoad Interface implementieren. Sie werden bei der Routenkonfiguration eingesetzt und führen Überprüfungen durch, bevor eine Route aktiviert wird.

Wozu werden Guards verwendet?

  1. Zugriffsschutz: Verhindere, dass nicht autorisierte Nutzer:innen auf geschützte Bereiche zugreifen.
  2. Benutzerfreundlichkeit: Leite Nutzer:innen automatisch zur Login-Seite um, wenn sie nicht angemeldet sind.
  3. Rollenbasierte Zugriffssteuerung: Erlaube den Zugriff auf bestimmte Routen nur Nutzer:innen mit spezifischen Rollen.
  4. Datenschutz: Stelle sicher, dass sensible Informationen nur autorisierten Personen zugänglich sind.

Class-Based Angular Auth Guards: Der klassische Ansatz

Bevor wir in die Details eintauchen, ist es wichtig zu erwähnen, dass Angular ein praktisches Command Line Interface (CLI) bietet, mit dem du Services, Guards und vieles weitere generieren kannst. Für dieses Beispiel werden wir den AuthService mit der CLI erstellen, den Auth Guard jedoch manuell implementieren. Der Grund dafür ist, dass in neueren Angular-Versionen die funktionale Implementierung der Standard für Auth Guards ist, und die CLI uns hier keinen Mehrwert bieten würde.

ng generate service auth

1. Auth Service

// src/app/auth.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private isAuthenticated = false;

  login(): void {
    // Hier würde normalerweise die tatsächliche Authentifizierungslogik stehen
    this.isAuthenticated = true;
  }

  logout(): void {
    this.isAuthenticated = false;
  }

  isLoggedIn(): boolean {
    return this.isAuthenticated;
  }
}

2. Auth Guard

// src/app/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (this.authService.isLoggedIn()) {
      return true;
    } else {
      // Umleitung zur Login-Seite, wenn nicht eingeloggt
      return this.router.createUrlTree(['/login']);
    }
  }
}

3. Routenkonfiguration

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { AuthGuard } from './auth.guard';

export const routes: Routes = [
  { 
    path: 'login', 
    loadComponent: () => import('./login.component').then(m => m.LoginComponent)
  },
  { 
    path: 'admin', 
    loadComponent: () => import('./admin.component').then(m => m.AdminComponent),
    canActivate: [AuthGuard] 
  },
  { 
    path: '', 
    redirectTo: '/login', 
    pathMatch: 'full' 
  }
];

Dann importieren wir diese Routen in unsere Hauptkomponente:

// src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { routes } from './app.routes';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: '<router-outlet></router-outlet>'
})
export class AppComponent { }

Schließlich müssen wir sicherstellen, dass unsere main.ts Datei die Anwendung korrekt bootstrapt:

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes)
  ]
}).catch(err => console.error(err));

4. Login Component

Eine beispielhafte Implementierung der Login-Komponente könnte wie folgt aussehen:

// src/app/login.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <h2>Login</h2>
    <form (ngSubmit)="onSubmit()">
      <div>
        <label for="username">Benutzername:</label>
        <input type="text" id="username" [(ngModel)]="username" name="username" required>
      </div>
      <div>
        <label for="password">Passwort:</label>
        <input type="password" id="password" [(ngModel)]="password" name="password" required>
      </div>
      <button type="submit">Anmelden</button>
    </form>
  `
})
export class LoginComponent {
  username: string = '';
  password: string = '';

  constructor(private authService: AuthService, private router: Router) {}

  onSubmit() {
    this.authService.login();
    this.router.navigate(['/admin']);
  }
}

5. Admin Component

Und hier ist eine einfache Implementierung einer Admin-Komponente:

// src/app/admin.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-admin',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h2>Admin Dashboard</h2>
    <p>Hallo und willkommen im Adminbereich 👋</p>
    <button (click)="logout()">Logout</button>
  `
})
export class AdminComponent {
  constructor(private authService: AuthService, private router: Router) {}

  logout() {
    this.authService.logout();
    this.router.navigate(['/login']);
  }
}

Aktualisierte Routenkonfiguration

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { LoginComponent } from './login.component';
import { AdminComponent } from './admin.component';

export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'admin', component: AdminComponent, canActivate: [AuthGuard] },
  { path: '', redirectTo: '/login', pathMatch: 'full' }
];

In dieser Konfiguration ist die Admin-Route durch den AuthGuard geschützt. Nur eingeloggte Benutzer können auf diese Seite zugreifen.

Mit dieser Implementierung hast du einen robusten, klassenbasierten Auth Guard erstellt, der den Zugriff auf geschützte Routen in deiner Angular-Anwendung kontrolliert. In den nächsten Abschnitten werden wir uns modernere Ansätze ansehen, einschließlich der funktionalen Implementierung von Auth Guards in neueren Angular-Versionen.

Functional Angular Auth Guards: Der moderne Weg

Mit Angular 14+ wurde ein neuer, funktionaler Ansatz für Guards eingeführt. Dieser ist kürzer, übersichtlicher und oft einfacher zu testen. Um von unserem klassenbasierten Ansatz zu diesem moderneren Ansatz zu wechseln, müssen wir folgende Änderungen vornehmen:

Generieren eines neuen funktionalen Guards mit der Angular CLI:

ng generate guard auth-functional

Dies erstellt eine neue Datei auth-functional.guard.ts mit einer funktionalen Implementierung.

Die generierte auth.guard.ts Datei sollte wie folgt aussehen und angepasst werden:

// src/app/auth-functional.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  } else {
    // Umleitung zur Login-Seite, wenn nicht eingeloggt
    return router.createUrlTree(['/login']);
  }
};

In der Routenkonfiguration (app.routes.ts) ersetzen wir den klassenbasierten Guard durch den neuen funktionalen Guard:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './auth-functional.guard';  // Importiere den neuen funktionalen Guard
import { LoginComponent } from './login.component';
import { AdminComponent } from './admin.component';

export const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'admin', component: AdminComponent, canActivate: [authGuard] },  // Verwende den funktionalen Guard
  { path: '', redirectTo: '/login', pathMatch: 'full' }
];

Der funktionale Ansatz macht den Code nicht nur kürzer, sondern auch flexibler. Du kannst leichter Parameter übergeben und komplexere Logik implementieren, ohne eine ganze Klasse definieren zu müssen.

Beachte, dass der Rest deiner Anwendung, einschließlich des AuthService und der Komponenten, unverändert bleibt. Der Hauptunterschied liegt in der Implementierung und Verwendung des Guards selbst.

Bonus: Implementierung von rollenbasiertem Zugriff

Guards können auch verwendet werden, um rollenbasierten Zugriff zu implementieren. Hier ein kurzes Beispiel, wie du den funktionalen Guard erweitern kannst, um bestimmte Rollen zu überprüfen:

Erweitere zunächst den AuthService um eine Methode zur Rollenüberprüfung:

// src/app/auth.service.ts
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  // ... bestehender Code ...

  getUserRole(): string {
    // In einer realen Anwendung würde dies die Rolle des angemeldeten Benutzers zurückgeben
    return 'admin';
  }
}

Passe den funktionalen Guard an, um Rollen zu überprüfen:

// src/app/auth-functional.guard.ts
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';

export const roleGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  const requiredRole = route.data['requiredRole'] as string;

  if (authService.isLoggedIn() && authService.getUserRole() === requiredRole) {
    return true;
  } else {
    // Umleitung zur Login-Seite oder zu einer "Zugriff verweigert" Seite
    return router.createUrlTree(['/login']);
  }
};

Verwende den Guard in den Routen und gib die erforderliche Rolle an:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { roleGuard } from './auth-functional.guard';
import { AdminComponent } from './admin.component';

export const routes: Routes = [
  // ... andere Routen ...
  { 
    path: 'admin', 
    component: AdminComponent, 
    canActivate: [roleGuard],
    data: { requiredRole: 'admin' }
  }
];

Mit dieser Implementierung wird der Zugriff auf die Admin-Komponente nur Benutzern mit der Rolle ‚admin‘ gewährt. Du kannst diesen Ansatz leicht erweitern, um mehrere Rollen oder komplexere Berechtigungsstrukturen zu unterstützen.

Praxis-Beispiel: Authentifizierung mit Supabase und Angular Auth Guards

Lass uns das Ganze noch praktischer gestalten und ein komplettes Beispiel für eine Authentifizierung mit Supabase und Angular Auth Guards durchgehen. Supabase ist eine Open-Source-Alternative zu Firebase und bietet eine robuste Authentifizierungslösung.

Schritt 1: Supabase Projekt einrichten

  1. Gehe zu supabase.com und erstelle ein neues Projekt.
  2. Nachdem das Projekt erstellt wurde, gehe zu den Projekteinstellungen und notiere dir die Project URL und den anon public API Key. Du wirst diese später in deiner Angular-Anwendung benötigen.

Schritt 2: Supabase Authentifizierung konfigurieren

  1. In deinem Supabase-Projekt, gehe zu „Authentication“ > „Providers“.
  2. Aktiviere „Email“ als Authentifizierungsprovider.
  3. Optional: Konfiguriere weitere Authentifizierungsprovider wie Google, GitHub, etc. nach Bedarf.

Schritt 3: Angular-Projekt vorbereiten

Installiere die notwendigen Pakete:

npm install @supabase/supabase-js

Schritt 4: Umgebungsvariablen einrichten

Erstelle oder bearbeite die Datei src/environments/environment.ts:

export const environment = {
  production: false,
  supabaseUrl: 'DEINE_SUPABASE_PROJECT_URL',
  supabaseKey: 'DEIN_SUPABASE_ANON_PUBLIC_KEY'
};

Ersetze die Platzhalter mit deinen tatsächlichen Supabase-Projektdaten.

Schritt 5: Supabase-Service erstellen

Erstelle einen Supabase-Service:

import { Injectable } from '@angular/core';
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { environment } from '../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class SupabaseService {
  private supabase: SupabaseClient;

  constructor() {
    this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey);
  }

  async signUp(email: string, password: string) {
    return this.supabase.auth.signUp({ email, password });
  }

  async signIn(email: string, password: string) {
    return this.supabase.auth.signInWithPassword({ email, password });
  }

  async signOut() {
    return this.supabase.auth.signOut();
  }

  getUser() {
    return this.supabase.auth.getUser();
  }
}

Schritt 6: Auth Guard implementieren

Implementiere den Auth Guard mit Supabase:

import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { SupabaseService } from './supabase.service';

export const supabaseAuthGuard = () => {
  const supabaseService = inject(SupabaseService);
  const router = inject(Router);

  return supabaseService.getUser().then(({ data: { user } }) => {
    if (user) {
      return true;
    } else {
      return router.createUrlTree(['/login']);
    }
  });
};

Schritt 7: Routenkonfiguration

In deiner Routenkonfiguration verwendest du den Guard wie folgt:

const routes: Routes = [
  { path: 'profile', component: ProfileComponent, canActivate: [supabaseAuthGuard] }
];

Schritt 8: Login-Komponente erstellen

Du kannst die zuvor erstellte Komponente verwenden und um die Login-Funktionalität des SupabaseService erweitern.

import { Component } from '@angular/core';
import { SupabaseService } from './supabase.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  template: `
  <form (ngSubmit)="onSubmit()">
    <div>
      <label for="email">E-Mail Adresse:</label>
      <input type="text" id="email" [(ngModel)]="email" name="email" required>
    </div>
    <div>
      <label for="password">Passwort:</label>
      <input type="password" id="password" [(ngModel)]="password" name="password" required>
    </div>
    <button type="submit">Login</button>
  </form>
  `
})
export class LoginComponent {
  email: string = '';
  password: string = '';

  constructor(private supabaseService: SupabaseService, private router: Router) {}

  async onSubmit() {
    try {
      const { error } = await this.supabaseService.signIn(this.email, this.password);
      if (error) throw error;
      this.router.navigate(['/profile']);
    } catch (error) {
      console.error('Error logging in:', error);
    }
  }
}

Mit dieser Implementierung hast du nun eine vollständige Authentifizierungslösung mit Supabase in deiner Angular-Anwendung. Der supabaseAuthGuard schützt die Routen, die nur für angemeldete Benutzer zugänglich sein sollen.

Denk daran, in einer Produktionsumgebung zusätzliche Sicherheitsmaßnahmen zu implementieren, wie das Handling von Fehler-Szenarien, das Implementieren von CSRF-Schutz und das sichere Speichern von Benutzer-Tokens.

Tipps und Best Practices

Hier noch einige Tipps, wie du das Beste aus deinen Angular Auth Guards herausholst:

  1. Kombiniere Guards: Du kannst mehrere Guards für eine Route verwenden, um komplexere Zugriffslogiken zu implementieren. Siehe dazu unser Beispiel für rollenbasierten Zugriff.
  2. Nutze Route Data: Übergib zusätzliche Informationen an deine Guards über die Route-Data, um sie flexibler zu gestalten.
  3. Asynchrone Überprüfungen: Guards unterstützen asynchrone Operationen. Nutze das, um API-Calls oder komplexe Überprüfungen durchzuführen.
  4. Fehlerbehandlung: Implementiere eine gute Fehlerbehandlung in deinen Guards, um unerwartete Situationen abzufangen.

Auth Guards: Die Türsteher für deine App

Angular Auth Guards sind ein mächtiges Werkzeug, um die Sicherheit und Benutzererfahrung deiner Anwendung zu verbessern. Ob du den klassischen class-based Ansatz oder die moderne funktionale Implementierung wählst – mit Auth Guards stellst du sicher, dass deine Nutzer:innen nur das sehen, was sie sehen sollen.

Die Integration mit Diensten wie Supabase macht es noch einfacher, robuste Authentifizierungslösungen zu implementieren. Experimentiere mit den verschiedenen Möglichkeiten und finde den Ansatz, der am besten zu deinem Projekt passt.

Denk daran: Sicherheit ist ein kontinuierlicher Prozess. Bleib auf dem Laufenden über die neuesten Best Practices und Sicherheitsempfehlungen für Angular-Anwendungen. Mit gut implementierten Auth Guards machst du einen großen Schritt in Richtung einer sicheren und benutzerfreundlichen App.

Du benötigst Hilfe bei der Implementierung komplexer Auth Guards und rollenbasierten Zugriffssystemen in deiner Angular-Anwendung? Wir bei Aptex sind Angular-Expert:innen und helfen dir gern bei der Optimerung deiner Apps. Buche noch heute einen Termin und lass uns quatschen!