L'authentification

L'application développée en backend nécessite une authentification pour fonctionner. Les routes utilisées auront besoin d'un jeton valide pour y accéder. Ce jeton va s'obtenir en envoyant un couple email + password sur une route spécifique avec la méthode POST. Ce jeton sera valide pendant 24h. Il faudra ensuite l'envoyer à chaque requête vers le serveur pour prouver que l'utilisateur est connecté.

L'URL pour récupérer un jeton est : http://localhost:3000/users/login

Nous allons voir comment gérer ceci dans une application Angular.

Angular Route Guard

Angular met à disposition des développeurs plusieurs méthodes pour sécuriser les routes de l'application. Ces interfaces sont :

  • CanActivate : pour contrôler l'accès à une route.
  • CanActivateChild : similaire à la précédente, mais s'applique sur l'ensemble des routes filles de la route ciblée.
  • CanLoad : pour la mise en place du lazy loading
  • CanDeactivate : pour empêcher un utilisateur de quitter une route
  • Resolve : pour s'assurer que toutes les données nécessaires sont bien présentes

Les services qui implémentent ces interfaces afin de déterminer si une partie de l'application doit être accessible ou non à un utilisateur est appelé Guard dans l'univers angular.

Nous allons donc créer un service et implementer l'interface CanActivate disponible dans le package @angular/router. L'interface CanActivate oblige à définir la méthode canActivate(): boolean. Cette méthode doit renvoyer true si l'utilisateur est autorisé à passer, false sans les autres cas. Exemple :

@Injectable()
export class AuthGuard implements CanActivate {
    constructor(private myService: MyService) { }

    canActivate(): boolean {
        if (this.myService.userAuthorized) {
            return true;
        }
        return false;
    }
}

Ensuite, dans le module de routing, il faut ajouter sur la route protégée la clè canActivate et lui passer en paramètre le service guard précédemment créé :

const routes: Routes = [
    { path: 'my-component', component: MyComponent, canActivate: [AuthGuard] },
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Attention : le paramètre canActivate de la route prend un tableau de guards comme paramètre.

TP : Développement d'un module de connexion

Dans cette partie nous allons développer le module LoginModule qui permettra à un utilisateur de se connecter sur l'application. Lors de la connexion, nous allons récupérer un token qui sera stocké. Nous ajouterons également une route protégée dans le module principal simplement pour le test.

  1. Créer un module LoginModule
  2. Créer le model UserLogin avec comme propriété email et password qui représente les données à envoyer à la route login
  3. Créer le model UserToken avec comme propriété userId et token qui représente la réponse de la route login coté backend
  4. Créer un service LoginService avec une propriété userToken et une méthode login
  5. Créer un component LoginComponent qui sera le formulaire de connexion de l'utilisateur avec deux champs email et password. Le formulaire doit faire appel à la méthode login de LoginService
  6. La méthode login de LoginService doit envoyer une requête au serveur sur /users/login afin de récupérer un token. La réponse de la requête doit être stockée dans la propriété userToken du service.
  7. Créer un service Guard pour vérifier que l'utilisateur à bien récupéré un token. Si l'utilisateur n'a pas de token, le rediriger le sur le formulaire de login et faire apparaître une notification toastr pour l'en informer
  8. Créer un fichier de routing pour le module LoginModule avec comme chemin login pour aller sur le component login
  9. Ajouter un component MissionListComponent qui affiche juste un titre h1. Ajouter ce component dans le module principal AppModule
  10. Ajouter une route mission-list dans le fichier de routing principal AppRoutingModule qui redirige vers MissionListComponent. Ce component doit être protégé par le service Guard.
  11. Dans la vue de AppComponent mettre uniquement le <router-outlet></router-outlet>
  12. N'oubliez pas d'importer votre module LoginModule dans le module principal

Résultat attendu :

auth_s1

auth_s2

Le Local Storage

Pour le moment notre authentification n'est pas persistante, cela signifie que si l'utilisateur recharge sa page il devra se reconnecter.

Pour faire en sorte que l'utilisateur ne se reconnecte pas à chaque rechargement de l'application nous allons devoir stocker le token de manière persistante. Pour cela nous allons utiliser une fonctionnalité disponible dans les navigateurs : le LocalStorage. Il permet aux développeurs de stocker des informations dans le navigateur. Au rechargement de la page, l'application pourra alors récupérer les informations qui y sont stockées.

Pour l'utiliser vous devez appeler la variable localStorage disponible n'importe où dans l'application. Les méthodes pour gérer le localStorage sont :

  • setItem(key: string, value: string) pour stocker une variable
  • getItem(key: string) pour récupérer une variable
  • removeItem(key: string) pour supprimer une variable

Attention : le localstorage ne peut stocker que des chaînes de caractères et pas des variables complexes comme des objets. Pour stocker des objets il faudra alors les sérialiser.

Exemple :

// Stockage de la variable userToken
const userTokenSerialized: string = JSON.stringify(this.userToken); // sérialisation 
localStorage.setItem('userToken', userTokenSerialized);

// Récupération de la variable userToken
const userTokenLocalStorage: string = localStorage.getItem('userToken');
const userToken = JSON.parse(userTokenLocalStorage); // désérialisation 

// Suppression de la variable userToken
localStorage.removeItem('userToken');

TP : Persistance du token

  1. Ajouter une méthode saveTokenToLocalStorage dans le service LoginService qui stockera l'objet userToken dans le Local Storage
  2. Ajouter une méthode restoreTokenFromLocalStorage dans le service LoginService pour restaurer l'objet userToken depuis le Local Storage
  3. À la connexion de l'utilisateur, n'oubliez pas d'appeler la fonction pour stocker le userToken dans le Local Storage
  4. Dans le component principal AppComponent, implémenter l'interface OnInit et ajouter la méthode ngOnInit()
  5. La méthode ngOnInit() sera appelée au démarrage de l'application. Pour restaurer le token vous devez appeler la méthode restoreTokenFromLocalStorage() du service LoginService
  6. Ajouter une méthode clearTokenFromLocalStorage() dans le service LoginService qui va supprimer l'objet userToken depuis le Local Storage. Nous utiliserons cette méthode plus tard dans le TP.

Vous pouvez alors constater qu'une fois connecté, le rechargement de la page mission-list ne redirige pas sur la page login. Le token est bien restauré au démarrage.

TP : Http Interceptor

Pour prouver au serveur que nous avons l'autorisation d'utiliser l'application nous devons envoyer à chaque requête HTTP le token dans le header Authorization sous la forme Authorization: Bearer <token>. Pour faire cela Angular met à notre disposition un mécanisme appelé HttpInterceptor qui va modifier la requête pour nous à chaque appel de HttpClient.

Pour l'utiliser nous devons ajouter un nouveau service qui implémente l'interface HttpInterceptor qui oblige l'ajout d'une méthode intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>. Cette méthode sera executée à chaque fois qu'une requête est envoyé. C'est ici que nous allons ajouter notre token.

Ajouter le service TokenInterceptorService, comme ceci :

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

import { LoginService } from './login.service';

@Injectable()
export class TokenInterceptorService implements HttpInterceptor {
    constructor(private loginService: LoginService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const userToken = this.loginService.userToken;
        let newHeaders = req.headers;
        if (userToken) {
            newHeaders = newHeaders.append('Authorization', `Bearer ${userToken.token}`);
        }

        return next.handle(req.clone({headers: newHeaders}));
    }
}

Il faut ensuite déclarer cet intercepteur dans notre LoginModule dans la partie providers, comme ceci :

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TokenInterceptorService } from './token-interceptor.service';
...
providers: [
    LoginService,
    AuthGuard,
    { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptorService, multi: true }
]
...

Dorénavant, à chaque requête HTTP que notre application va lancer, il y aura un en-tête contenant le token de l'utilisateur si celui-ci s'est connecté.