Angular 4 – Gestion de l’authentification et des habilitations

Ce post a pour objectif de vous fournir une solution technique 'from scratch' pour la gestion de l'authentification et des habilitations via angular 4.

Initialisation du projet

En quelques commandes nous allons tout d'abord créer une structure de base de projet comme suit :

src
  \app
    \home : Notre page d'accueil
    \login : Notre page de connexion
    app-routing.module.ts : Le fichier des règles de routage
    app.module.ts : La configuration de notre application

Installation angular-cli via npm si ce n'est pas déjà fait

console

$ npm i -g angular-cli

Création du projet et de son arborescence

console

$ ng new angular4-authent --routing
$ cd angular4-authent
$ ng generate component login
$ ng g component home

Gestion du login - redirection

Tout d'abord nous allons mettre en place la navigation de base du projet.
L'objectif est d'arriver par défaut sur la page de login, puis suite à la saisie des identifiants d'être redirigé vers une page d'accueil, mentionnant l'identifiant de l'utilisateur.

Configuration de nos composants

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    HomeComponent
  ],
  imports: [
    FormsModule,
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Gestion du routage

On déclare le path de nos deux composants, ainsi qu'une redirection par défaut vers la page de connexion si l'on accède à la racine de l'application.

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from 'app/login/login.component';
import { HomeComponent } from 'app/home/home.component';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  {
    path: 'login',
    component: LoginComponent
  },
  {
    path: 'home',
    component: HomeComponent
  }
];

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

Création de la page de login

Nous partirons ici sur une structure de base simplifiée à adapter selon l'ergonomie recherchée.
login/login.component.html

<div class="col-md-6 col-md-offset-3">
  <div class="login-wrapper">
    <h2>Page de connexion</h2>
    <form name="form" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
      <div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
        <label for="username">Identifiant</label>
        <input type="text" class="form-control" name="username" id="username" [(ngModel)]="model.username" #username="ngModel" placeholder='Saisissez votre identifiant' required />
      </div>
      <div class="form-group" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
        <label for="password">Mot de passe</label>
        <input type="password" class="form-control" name="password" id="password" [(ngModel)]="model.password" #password="ngModel" placeholder='Saisissez votre mot de passe' required />
      </div>
      <div class="form-group">
        <button class="btn btn-primary pull-right" [disabled]="!f.form.valid">Connexion</button>
      </div>
    </form>
  </div>
</div>

login/login.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  model: any = {};

  constructor(
    private router: Router
  ) { }

  ngOnInit() { }

  login() {
    console.log('Tentative de connexion');

    // Vérifier que login/mdp sont correctes, par exemple par une requête à un service REST
    localStorage.setItem('user', JSON.stringify({login : this.model.username}));
    this.router.navigate(['/home']);
  }
}

Le stockage dans le locaStorage, nous permet de conserver les informations liées à l'utilisateur connecté.
Dans un cas réel, l'aspect session utilisateur serait géré via les appels RESTs nécessaires à l'alimentation en données des pages.

NB : Le localStorage permet notamment le partage de données multi-onglet

Gestion de la déconnexion

Nous rajoutons maintenant à la page d'accueil l'affichage de l'identifiant de l'utilisateur ainsi qu'un bouton de déconnexion.
home/home.component.html

<p>
  Welcome home {{getLogin()}} !
</p>
<a (click)="logout()">Se déconnecter</a>

home/home.component.ts

import { Router } from '@angular/router';

...

constructor(
  private router: Router
) { }

...

getLogin() {
  return JSON.parse(localStorage.getItem('user')).login;
}

logout() {
  console.log('Tentative de déconnexion');

  localStorage.removeItem('user');
  this.router.navigate(['/login']);
}

Gestion de l'authentification

La navigation est en place et permet d'accèder à la page d'accueil depuis la page de connexion.
Cependant si l'utilisateur appelle directement l'url '/home', il faut pouvoir vérifier que ce dernier est déjà connecté.
On créé donc un AuthGuard qui étend l'interface CanActivate et que l'on va placer sur les routes nécessitant un utilisateur connecté.

console

$ ng g guard service/auth/auth

service/auth/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { isNull } from 'util';

@Injectable()
export class AuthGuard implements CanActivate {

constructor(
  private router: Router
) { }

canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    // Récupération de l'utilisateur connecté
    const isLoggedIn = !isNull(localStorage.getItem('user'));

    if (!isLoggedIn) {
      // Si pas d'utilisateur connecté : redirection vers la page de login
      console.log('Vous n\'êtes pas connectés');
      this.router.navigate(['/login']);
    }
    return isLoggedIn;
  }
}

Ajout des restrictions de routes

Nous paramètrons maintenant l'application pour que la page d'accueil ne soit accessible qu'aux utilisateurs connectés.
app-routing.module.ts

import { AuthGuard } from 'app/service/auth/auth.guard';

...

{
  path: 'home',
  canActivate: [AuthGuard],
  component: HomeComponent
}

...

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

A partir de maintenant si un utilisateur attaque l'application directement sur la path '\home', sans s'être préalablement connecté, il est redirigé vers la page d'accueil.

Gestion de la redirection après authentification

Mais reprenons le cas ou l'utilisateur appelle directement une url spécifique de l'application, sans être connecté.
Le comportement attendu serait qu'il soit d'abord redirigé vers la page de connexion, puis une fois l'authentification vérifiée, qu'on le redirige vers l'url initiale. Pour cela nous allons devoir fournir à la méthode de login l'url vers laquelle il doit rediriger l'utilisateur si il s'authentifie avec succès.

Dans un premier temps ajoutons une nouvelle page à notre application

src
  \app
    \page

console

$ ng g component page

- Mise à jour de la route avec vérification de l'authentification

app-routing.module.ts

import { PageComponent } from 'app/page/page.component';

...

{
  path: 'page',
  canActivate: [AuthGuard],
  component: PageComponent
}

- Création du lien vers la page depuis home

home/home.component.html

<p>
<a routerLink="/page">Vous pouvez accèder à la page</a>
</p>

Passage de l'url demandée via le AuthGard

Lors de la redirection vers la page de login, le AuthGuard ajoute en paramètre d'url l'url actuelle.
service/auth/auth.guard.ts

canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
...
    this.router.navigate(['/login'], { queryParams: { redirectUrl: state.url }});
...
}

A l'appel de l'url '/page', si l'utilisateur est déconnecté, il est redirigé vers l'url '/login?redirectUrl=%2Fpage'.

NB : il est important de bien pointer sur '/login' et pas '', sinon la redirection liée à la route ne passe pas les paramètres d'url.

Gestion de la redirection au login

Il suffit maintenant lors de la tentative de connexion de récupèrer le paramètre d'url comme cible de redirection.

login/login.component.html

import { ActivatedRoute, Router } from '@angular/router';

...

constructor(
  private router: Router,
  private route: ActivatedRoute
) { }

...

  login() {
...
    // On récupère l'url de redirection
    const redirectUrl = this.route.snapshot.queryParams['redirectUrl'] || '/home';

    // On accède à la page souhaitée
    this.router.navigate([redirectUrl]);
  }

Gestion des roles

Maintenant nous savons naviguer dans l'application avec gestion de l'authentification.
Il nous reste donc à gérer les habilitations de l'utilisateur selon les pages.

Nettoyage et centralisation des méthodes

Tout d'abord un peu de nettoyage. On va reporter toute la gestion du stockage de l'utilisateur et de sa récupération dans un service dédié 'AuthService'.
console

$ ng g service service/auth/auth

- Intégration comme provider

app.module.ts

import { AuthService } from 'app/service/auth/auth.service';

...

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
  ],
  providers: [AuthService],
  bootstrap: [AppComponent]
})
export class AppModule { }

- Centralisation des méthodes de gestion de l'utilisateur connecté

service/auth/auth.service.ts

import { Injectable } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

@Injectable()
export class AuthService {

  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) { }

  login(loginForm: any) {
    console.log('Tentative de connexion');

    this.setUser({login : loginForm.username});

    // On récupère l'url de redirection
    const redirectUrl = this.route.snapshot.queryParams['redirectUrl'] || '/home';

    // On accède à la page souhaitée
    this.router.navigate([redirectUrl]);
  }

  logout() {
    console.log('Tentative de déconnexion');

    this.clearUser();
    this.router.navigate(['/login']);
  }

  getUser() {
    return JSON.parse(localStorage.getItem('user'));
  }

  setUser(user: any) {
    localStorage.setItem('user', JSON.stringify(user));
  }

  clearUser() {
    localStorage.removeItem('user');
  }

}

- Import du service dans les composants

login/login.component.ts

import { Component, OnInit } from '@angular/core';

import { AuthService } from 'app/service/auth/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
  providers: [AuthService]
})
export class LoginComponent implements OnInit {

  model: any = {};

  constructor(
    private authService: AuthService
  ) { }

  ngOnInit() { }

  login() {
    this.authService.login(this.model);
  }
}

home/home.component.ts

import { Component, OnInit } from '@angular/core';

import { AuthService } from 'app/service/auth/auth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
  providers: [AuthService]
})
export class HomeComponent implements OnInit {

  constructor(
    private authService: AuthService
  ) { }

  ngOnInit() {
    console.log('Welcome home');
  }

  getLogin() {
    return this.authService.getUser().login;
  }

  logout() {
    return this.authService.logout();
  }
}

Créons des pages restreintes en droits

src
  \app
    \page-lecteur
    \page-redacteur

console

$ ng g component pageLecteur
$ ng g component pageRedacteur

Mise en place des roles

Bien évidement, dans le monde réel le AuthService ferait appel à un service REST retournant un utilisateur avec ses habilitations et vérifiant la concordance des mots de passe.
Pour l'exemple nous allons ajouter manuellement ces roles.

service/auth/auth.service.ts

login(loginForm: any) {
...
  // Ajout des roles au modèle utilisateur
  let rolesUser = [];
  if (loginForm.username === 'admin') {
    rolesUser = ['READ','WRITE'];
  } else if (loginForm.username === 'lecteur') {
    rolesUser = ['READ'];
  } else if (loginForm.username === 'redacteur') {
    rolesUser = ['WRITE'];
  }
  this.setUser({login : loginForm.username, roles : rolesUser});
...
}

Affichage conditionnel

Sur la page d'accueil, nous souhaitons que l'affichage des liens des pages soit conditionné en fonction des droits de l'utilisateur connecté.

Dans un premier temps ajoutons à notre service de gestion de l'utilisateur, une méthode de vérification des droits appellée transitivement depuis le composant 'home'.
service/auth/auth.service.ts

hasAnyRole(roles: string[]) {
  const user = this.getUser();

  for (const role of user.roles) {
    if (roles.includes(role)) {
      return true;
    }
  }
  return false;
}

home/home.component.ts

hasAnyRole(roles: string[]) {
  return this.authService.hasAnyRole(roles);
}

Puis dans la pages définissons les conditions d'affichage selon les droits.
home/home.component.html

<p *ngIf="hasAnyRole(['READ'])">
  <a routerLink="/pageLecteur">Vous pouvez accèder à la page de lecture</a>
</p>
<p *ngIf="hasAnyRole(['WRITE'])">
  <a routerLink="/pageRedacteur">Vous pouvez accèder à la page de rédaction</a>
</p>

Restriction du routage

Maintenant si l'utilisateur connait déjà le lien vers une page à la quelle il n'a pas accès. Il faut que la règle de routage le bloque et le redirige vers la page d'accueil.
Pour cela nous allons paramètrer les routes en y ajoutant des données, utilisables par le AuthGuard.

- Paramètrage des routes avec des data

app-routing.module.ts

import { PageLecteurComponent } from './page-lecteur/page-lecteur.component';
import { PageRedacteurComponent } from './page-redacteur/page-redacteur.component';

...

{
  path: 'pageLecteur',
  canActivate: [AuthGuard],
  component: PageLecteurComponent,
  data: {
    roles: ['READ']
  }
},
{
  path: 'pageRedacteur',
  canActivate: [AuthGuard],
  component: PageRedacteurComponent,
  data: {
    roles: ['WRITE']
  }
}

- Vérification des roles dans le AuthGuard

service/auth/auth.guard.ts

import { AuthService } from 'app/service/auth/auth.service';

...

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

...

canActivate(
  next: ActivatedRouteSnapshot,
  state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
...
  const roles = next.data['roles'];
  let hasRoles = true;
  if (roles) {
    hasRoles = this.authService.hasAnyRole(roles);
  }
  if(!hasRoles) {
    // Si l'utilisateur na pas les habilitations : redirection vers la page d'accueil
    console.log('Vous n\'avez pas les droits');
    this.router.navigate(['/home']);
  }

  return isLoggedIn && hasRoles;
}

Conlusion

Dans ce blog nous avons vu comment pouvoir gérer l'authentification et les habilitations sur une application Angular 4.
Pour parfaire cet exemple il reste bien sur à gérer les messages d'erreur, l'internationalisation, le branchement de l'authentification au serveur backend, ... Bref ce n'était que le commencement.

Biblio

https://angular.io/guide/router
https://github.com/angular/angular-cli#generating-and-serving-an-angular-project-via-a-development-server

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.