[Nouveau] Rejoignez la communauté Angular.fr !

Skip to content

Authentification sécurisée avec cookies HTTP-Only et Refresh Token dans Angular

Dans ce tutoriel, nous allons explorer une méthode hautement sécurisée pour gérer l'authentification dans une application Angular en utilisant des cookies HTTP-only pour stocker à la fois le token d'accès et le refresh token.

Imaginez que vous ayez deux coffres-forts virtuels ultra-sécurisés : un pour votre clé d'accès quotidienne (token d'accès) et un autre pour votre clé de secours (refresh token). C'est exactement ce que nous allons mettre en place pour protéger les données de vos utilisateurs !

Qu'est-ce qu'un cookie HTTP-only ?

Un cookie HTTP-only est un type spécial de cookie qui ne peut pas être accédé par les scripts côté client (comme JavaScript). Cela offre une protection supplémentaire contre les attaques de type Cross-Site Scripting (XSS).

Qu'est-ce qu'un refresh token ?

Un refresh token est un jeton d'authentification spécial utilisé pour obtenir un nouveau token d'accès lorsque celui-ci expire. Il permet de maintenir la session de l'utilisateur active sans avoir à se reconnecter fréquemment, tout en conservant un niveau de sécurité élevé.

Voici les principaux avantages d'utiliser un refresh token :

  1. Durée de vie plus longue : Contrairement aux tokens d'accès qui expirent rapidement, les refresh tokens peuvent avoir une durée de vie plus longue.

  2. Sécurité renforcée : Si un token d'accès est compromis, son utilisation est limitée dans le temps. Le refresh token, stocké de manière plus sécurisée, permet de générer de nouveaux tokens d'accès.

  3. Expérience utilisateur améliorée : L'utilisateur reste connecté plus longtemps sans avoir à saisir à nouveau ses identifiants.

  4. Révocation facilitée : En cas de compromission, il est plus facile de révoquer un refresh token, invalidant ainsi tous les futurs tokens d'accès.

Attention

Assurez-vous que votre backend est correctement configuré pour gérer les deux cookies HTTP-only avant de passer à l'implémentation côté Angular.

Service d'authentification

Voici une version mise à jour du service d'authentification :

typescript
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { User } from './user';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private http = inject(HttpClient)
  private _currentUser = signal<User | null>(null)
  currentUser = this._currentUser.asReadonly()
  isConnected = computed(() => this.currentUser() !== null)

  login(username: string, password: string): Observable<{
    user: User
  }> {
    return this.http.post<{
      user: User
    }>('/api/login', { username, password }, { withCredentials: true })
      .pipe(
        tap(response => {
          // Les deux tokens sont automatiquement stockés dans des cookies HTTP-only
          // Nous mettons à jour l'état de l'utilisateur connecté
          this._currentUser.set(response.user);
        })
      );
  }

  // Méthode pour rafraîchir les tokens. Utilisée par l'intercepteur HTTP
  revokeToken(): Observable<any> {
    return this.http.post<any>('/api/revoke-token', {}, { withCredentials: true })
      .pipe(
        tap(response => {
          // Les nouveaux tokens sont automatiquement stockés dans des cookies HTTP-only
          console.log('Tokens refreshed successfully');
        })
      );
  }

  logout(): Observable<any> {
    return this.http.post<any>('/api/logout', {}, { withCredentials: true })
      .pipe(
        tap(() => {
          // Le backend devrait supprimer les cookies
          this._currentUser.set(null);
        })
      );
  }
}
ts
export interface User {
    id: number;
    name: string;
    username?: string;
    email: string;
    address?: {
        street: string;
        suite: string;
        city: string;
        zipcode: string;
        geo: {
            lat: string;
            lng: string;
        }
    };
    phone?: string;
    website?: string;
    company?: {
        name: string;
        catchPhrase: string;
        bs: string;
    };
}

L'option { withCredentials: true } joue un rôle crucial dans notre système d'authentification basé sur des cookies HTTP-only. Voici pourquoi elle est importante :

  1. Envoi des cookies : Cette option permet à Angular d'envoyer les cookies avec chaque requête HTTP, même pour les requêtes cross-origin (CORS). Sans cette option, les cookies ne seraient pas envoyés pour les requêtes vers un domaine différent de celui qui héberge l'application.

  2. Réception des cookies : Elle autorise également le navigateur à accepter et stocker les cookies envoyés par le serveur en réponse à ces requêtes.

Ensuite, dans notre service d'authentification, nous utilisons un signal pour gérer l'état de l'utilisateur courant et un computed pour déterminer si l'utilisateur est connecté. Voici comment cela fonctionne :

  1. Signal pour l'utilisateur courant :

    typescript
    private _currentUser = signal<User | null>(null)
    currentUser = this._currentUser.asReadonly()
    • Nous utilisons un signal privé _currentUser pour stocker l'état de l'utilisateur connecté.
    • Nous exposons une version en lecture seule de ce signal via currentUser pour empêcher les modifications externes.
  2. Computed pour l'état de connexion :

    typescript
    isConnected = computed(() => this.currentUser() !== null)
    • Cette propriété calculée (computed) détermine si un utilisateur est connecté en vérifiant si currentUser n'est pas null.
    • Elle se met à jour automatiquement chaque fois que currentUser change.
  3. Gestion des tokens dans les méthodes :

    • Dans la méthode login, nous mettons à jour _currentUser avec les informations de l'utilisateur reçues du serveur.
    • Dans logout, nous réinitialisons _currentUser à null.
    • La méthode revokeToken ne modifie pas directement l'état de l'utilisateur, car elle est utilisée pour rafraîchir les tokens en arrière-plan.

Demander de rafraichir les tokens

Ce code implémente un intercepteur HTTP pour gérer l'authentification et le rafraîchissement automatique des tokens dans une application Angular.

Plus d'informations

Lire l'article sur les interceptors HTTP

typescript
import { HttpEvent, HttpHandlerFn, HttpHeaders, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
import { Observable, throwError, catchError, switchMap } from 'rxjs';

export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  const authService = inject(AuthService);
  
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        return authService.revokeToken().pipe(
          switchMap(() => {
            return next(req);
          }),
          catchError((refreshError) => {
            // Si le rafraîchissement échoue, déconnectez l'utilisateur
            authService.logout();
            return throwError(() => refreshError);
          })
        );
      }
      return throwError(() => error);
    })
  );
}

Voici les principaux objectifs de ce code :

  1. Gestion des erreurs d'authentification : L'intercepteur capture les erreurs HTTP 401 (Non autorisé), qui indiquent généralement que le token d'accès a expiré.

  2. Rafraîchissement automatique du token : Lorsqu'une erreur 401 est détectée, l'intercepteur tente automatiquement de rafraîchir le token en appelant la méthode revokeToken() du service d'authentification.

  3. Réessai de la requête originale : Si le rafraîchissement du token réussit, l'intercepteur réessaie automatiquement la requête originale qui a échoué, maintenant avec le nouveau token.

  4. Gestion des échecs de rafraîchissement : Si le rafraîchissement du token échoue (par exemple, si le refresh token est également expiré), l'utilisateur est déconnecté via authService.logout().

  5. Propagation des erreurs : Les erreurs qui ne sont pas liées à l'authentification (non-401) sont simplement propagées pour être gérées ailleurs dans l'application.

Pensez à ajouter l'interceptor dans la configuration de l'application

Dans votre app.config.ts, ajoutez l'interceptor dans la liste des fournisseurs en utilisant withInterceptors.

ts
import { ApplicationConfig, InjectionToken } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    )
  ]
};

Rappel sur switchMap

  1. Transformation de flux : switchMap permet de transformer un flux Observable en un autre flux Observable.

  2. Annulation des souscriptions précédentes : Lorsqu'une nouvelle valeur arrive dans le flux source, switchMap annule automatiquement la souscription à l'Observable précédent et souscrit au nouveau.

Plus d'informations sur switchMap

Utilisation du login dans un composant

Le composant de connexion reste classique:

typescript
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AuthService } from '../core/services/auth.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form (ngSubmit)="onSubmit()">
      <input [(ngModel)]="username" name="username" placeholder="Username" required>
      <input [(ngModel)]="password" name="password" type="password" placeholder="Password" required>
      <button type="submit">Login</button>
    </form>
  `
})
export class LoginComponent {
  private authService = inject(AuthService);
  private router = inject(Router);

  username = '';
  password = '';

  onSubmit() {
    this.authService.login(this.username, this.password).subscribe({
      next: () => {
        console.log('Logged in successfully');
        this.router.navigateByUrl('/');
      },
      error: (error: HttpErrorResponse) => {  
        console.error('Login failed', error);
        // Afficher un message d'erreur à l'utilisateur
      }
    });
  }
}

Chaque mois, recevez en avant-première notre newsletter avec les dernières actualités, tutoriels, astuces et ressources Angular directement par email !