Angular, ExpressJS | Authentification JWT avec Angular 10 et ExpressJS 4 (MySQL)

Je vais partir du principe que vous avez déjà votre Front avec Angular 10 et votre Back avec ExpressJS 4 et je ne montrerais pas la partie de création de ces deux projets.

Création du formulaire de login sous Angular

Le côté back du formulaire :

Rendez vous sur votre fichier formulaire.component.ts (Faites en fonction de vos composants…) et nous allons le construire comme sur l’exemple :

import { Component, OnInit } from '@angular/core';
// Import du constructeur de formulaire
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-login-form',
  templateUrl: './login-form.component.html',
  styleUrls: ['./login-form.component.scss']
})

export class LoginFormComponent implements OnInit {
  // Nos champs requis dans le formulaire
  email: String = '';
  password: String = '';
  // Le formulaire
  loginForm;

  constructor(private formBuilder: FormBuilder) {
    // Lors de la création du composant nous définissons le formulaire, sans validation, le tutoriel ne traitera pas de cette partie
    this.loginForm = this.formBuilder.group({
      email: '',
      password: ''
    });
  }

  ngOnInit(): void {}
}

Nous voyons que nous importons le constructeur de formulaire qui n’est d’autre que le FormBuilder, nous instancions nos variables de formulaire et nous définissons la base du formulaire à partir du FormBuilder.

Le côté front du formulaire :

<div id="login">
    <h1>Login form</h1>
    // La notion formGroup défini vers quel variable est construit le formulaire dans le côté back
    <form [formGroup]="loginForm">
        <div class="form-input">
            <label for="email">Email</label>
            // La notion formControlName permet de dire à notre formBuilder vers quelle variable les données du input sont liées
            // Le tag [(ngModel)]="email" permet lors de la modification de l'email dans l'input, d'actualiser la données de la variable
            <input type="text" formControlName="email" [(ngModel)]="email">
        </div>
        <div class="form-input">
            // Voir email ce sont les mêmes explications
            <label for="password">Password</label>
            <input type="password" formControlName="password" [(ngModel)]="password">
        </div>
        // Le (click)="submit" indique que lors du clic sur le bouton, la fonction submit() se lancera
        <button type="button" (click)="submit()">Submit</button>
    </form>
</div>

Nous créeons un formulaire basique avec les directives formGroup, formControlName et ngModel qui sont nouvelles et expliquées dans le code, nous allons poursuivre et créer le service de login qui va intéragir avec le serveur ExpressJS

Service de login et fonction de soumission au serveur :

Avant d’écrire la fonction de soumission des identifiants au serveur, il faut écrire un service de login. Cela nous permettra si on le désire de le réutiliser à un autre endroit de nos composants Angular.

import { Injectable } from '@angular/core';
// Permet d'envoyer des requêtes http
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class LoginService {
  // L'utilisateur sera stocké ici
  currentUser;
  // Son token JWT ici
  token;
  // l'url vers l'api, ne pas oublier le http:// /!\
  urlApi = 'http://localhost:3000/api/';

  constructor(private http: HttpClient) { }

  login(data) {
    // Cette fonction sera appelée dans notre soumission de formulaire, elle permet d'appeler l'api en post sur la route 'http://localhost:3000/api/login', le fait de retourner this.http.post renverra un Observable que l'on pourra souscrire pour recevoir les données dans le formulaire
    return this.http.post(this.urlApi + 'login', data);
  }
}

Maintenant que nous avons notre service qui est réutilisable, ne pas oublier de l’importer dans notre formulaire.component.ts pour pouvoir l’appeler.

Il est maintenant temps de créer la fonction de soumission.

import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { LoginService } from '../services/login-service.service'

@Component({
  selector: 'app-login-form',
  templateUrl: './login-form.component.html',
  styleUrls: ['./login-form.component.scss']
})
export class LoginFormComponent implements OnInit {
  email: String = '';
  password: String = '';
  loginForm;
  currentUser;

  // Nous appelons le loginService pour faire appel à sa fonction login
  constructor(private formBuilder: FormBuilder, private loginService: LoginService) {
    
    this.loginForm = this.formBuilder.group({
      email: '',
      password: ''
    });
  }

  ngOnInit(): void {}

  // Fonction lancée au submit du formulaire
  submit(){
    // Nous créeons un objet data avec les champs du formulaire rempli
    let data = { email: this.email, password: this.password }
    // Nous appelons le login service sur la méthode login avec l'objet data
    // Le login service va envoyer la requête HTTP en post au serveur avec les données de l'utilisateur
    // Nous appliquons la méthode subscribe qui va écouter l'observable retourné
    // La première fonction (value) est le retour de la valeur, il existe aussi la deuxième (err) qui permet de gérer l'erreur et ne troisième () => {} qui permet de définir du code lorsque le subscriber est fini
    this.loginService.login(data).subscribe(
      (value) => {
        this.loginService.currentUser = value
        this.loginService.token = value.token,
        this.currentUser = value
        localStorage.setItem('token', value.token)
      },
      (err) => console.log(err)
    )
  }

}

Si vous n’êtes pas familier avec les Observables vous pouvez allez sur la documentation officielle Angular qui vous l’expliquera.

Maintenant que nous avons notre formulaire construit, un service qui interroge le serveur, et un subscriber qui nous retourne la donnée du serveur, nous pouvons maintenant travailler sur le serveur en lui même.

Serveur ExpressJS

Comme pour le front d’Angular, je vais rentre dans le vif du sujet tout de suite, si besoin de connaitre ExpressJS, allez sur le site directement.

Pour faire fonctionner notre authentification nous avons besoin de la librairie mysql et celle de JWT pour générer un token. Je vous laisse les installer pour continuer la suite.

Les requêtes Cross Origin peuvent aussi gêner, installez CORS

Service de la base de données MySQL

Voici le service de BDD a mettre dans un fichier à part pour appeler la base de données facilement.

var mysql      = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'root',
  password : '',
  database : 'angular-express'
});
 
connection.connect(function(err) {
    if (err) throw err;
});

module.exports = connection;

Remplacez avec vos données.

Le serveur

const express = require('express')
// Permet de recevoir les requêtes en Cross origin
const cors = require("cors");
const app = express()
const port = 3000
// Permet de convertir les données reçues en HTTP en variable lisable
const bodyParser = require('body-parser');
//Permet d'utiliser la base de données, changez en fonction de vous
var db = require('./database');
// Permet d'utiliser le JWT
var jwt = require('jsonwebtoken');

// On convertie les données reçues
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// On permet de cross origin (Angular en port 4200 et l'api en 3000...)
app.use(cors());

// Notre fameuse route de login
app.post('/api/login', (req, res) => {
  // On recupère les variables envoyées par notre formulaire à partir du service de login
  let { email, password } = req.body;
  // On instancie un variable qui accueil l'utilisateur
  let user;
  // Nous faisons notre requête nous vérifier que l'utilisateur existe bien sous l'email envoyée
  db.query('SELECT * FROM user WHERE email = ?', [email], function(err, result) {
    if (err) throw err;
    // On stocke notre utilisateur dans notre variable
    user = result[0]
    // Petite parenthèse, pour ce tuto les mdp ne sont pas hashé dans le base, ce n'est pas le sujet, alors nous comparons sans décrypter les mots de passe
    if(user.password == password){
      // Si toutes les informations sont ok alors on génère le jwt avec les données de l'utilisateur (on evite de mettre le mot de passe c'est pas sécurisé), et on ajoute un mot clé 'secret', à changer selon vos envies, il faut qu'il soit secret justement, on définit le jwt valide une heure
      var token = jwt.sign({ id: user.id, email: user.email, name: user.name }, 'secret', { expiresIn: '1h' });
      // On donne le token à l'user et on retourne l'user
      user.token = token
      res.send(user)
    } else {
      // Sinon on prévient l'utilisateur que personne n'est trouvé
      res.send(["User don't find"])
    }
  });
})

// Le serveur écoute
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Notre formulaire est maintenant capable de recevoir l’utilisateur qui souhaite se connecter, lui attribuer un token qui prouve que l’utilisateur a bien saisi ses identifiants, et le service login a stocké tout ca en mémoire pour être réutilisé où l’on veut, il est maintenant temps de voir les routes de l’api sécurisées via le token.

Routes sécurisées via JWT sur ExpressJS

Prenons cette route en exemple :

app.get('/api/dataNeedLogged', (req, res) => {
  res.send({ message: "You are logged with JWT you can see the data"})
})

Elle nous renvoi un simple message mais l’a n’est pas le sujet, elle n’a rien de différent de la route de login pour le moment, nous allons la sécuriser. Il faut mettre en place un Middleware d’authentification JWT. Si je devais expliquer en gros qu’est ce que le Middleware, c’est comme une fonction qui se lance avant votre fonction principale, voir exemple :

//Nous importons notre middleware
let authMiddleware = require('./auth_jwt.middleware')

//Nous ajoutons notre middleware AVANT la fonction principale de notre route
app.get('/api/dataNeedLogged', authMiddleware, (req, res) => {
  res.send({ message: "You are logged with JWT you can see the data"})
})

Contenu du middleware :

//Imports des dependances
const jwt = require("jsonwebtoken")
const db = require('./database')

//Voici la fonctionqui va être lancée avant la fonction principale de la route
//Elle dispose des mêmes paramètres que notre route à une EXCEPTION, le paramètre NEXT, il est obligatoire et permet de dire au MIDDLEWARE ok tu peux passer à la fonction principale
const auth = (req, res, next) => {
    try {
        // Nous récupérons le token de la requête HTTP
        const token = req.header('Authorization').replace("Bearer ", "")
        // On décode le token avec la clef "secret", remplacez avec votre clef secrète !
        const decoded = jwt.verify(token, "secret")

        //Grâce au token décodé nous avons l'id de l'utilisateur, nous pouvons donc le retrouver
        //et juger si oui ou non il a le droit d'accès
        db.query('SELECT * FROM user WHERE id = ?', [decoded.id], function (err, result) {
            if (err) throw err;
            user = result[0]
            //Si aucun user trouvé on passe à l'erreur 401
            if (!user) {
                throw new Error()
            }
            req.token = token
            req.user = user
            //On passe à la requête principale
            next()
        });
        
    } catch (e) {
        res.status(401).send({ error: 'Please authenticate.' })
    }
}

//On exporte la fonction pour l'inclure avant notre fonction de route
module.exports = auth;

Il faut maintenant qu’Angular nous envoie à chaque requête le token d’authentification, cela se gère avec un interceptor.

Angular, ajouter le token JWT à la requête HTTP

Voici la classe d’interceptor qui va permettre d’ajouter le token à nos requêtes HTTP :

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

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor() {}

  //Avant l'envoi d'une requête HTTP l'interceptor va ajouter le token à l'authentification
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    //Récupération du token
    const token = localStorage.getItem('token');
    //On clone la requête en y ajoutant un headers Authorization avec la chaine de caractères Bearer 
    suivie du token
    req = req.clone({
      setHeaders: {
        'Authorization': `Bearer ${token}`
      },
    });

    return next.handle(req);
  }
}

Pour qu’il intercepte toutes les requête il faut aller dans app.module.ts et ajouter le provider suivant :

//On importe HTTP_INTERCEPTORS
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
//Et notre interceptor
import { AuthInterceptor } from './interceptor/auth.interceptor'

...
...
...
//Et on dit à Angular de l'utiliser
providers: [
    {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
],

Voilà nos requêtes venant d’Angular sont prêtes à communiquer avec les routes sécurisée de l’API ExpressJS, le tutoriel est maintenant fini.

Merci d’avoir lu, n’hésitez pas à partager !