Comment faire une authentification JWT avec un SPA angulaire 6
Publié: 2022-03-11Aujourd'hui, nous verrons à quel point il est facile d'intégrer l'authentification par jeton Web JSON (JWT) dans votre application monopage (SPA) Angular 6 (ou ultérieure). Commençons par un peu de contexte.
Que sont les jetons Web JSON et pourquoi les utiliser ?
La réponse la plus simple et la plus concise ici est qu'ils sont pratiques, compacts et sécurisés. Examinons ces affirmations en détail :
- Pratique : L'utilisation d'un JWT pour l'authentification au back-end une fois connecté nécessite de définir un en-tête HTTP, une tâche qui peut être facilement automatisée via une fonction ou une sous-classe, comme nous le verrons plus tard.
- Compact : un jeton est simplement une chaîne encodée en base64, contenant quelques champs d'en-tête et une charge utile si nécessaire. Le JWT total est généralement inférieur à 200 octets, même s'il est signé.
- Sécurisé : Bien que cela ne soit pas obligatoire, une excellente fonctionnalité de sécurité de JWT est que les jetons peuvent être signés à l'aide du cryptage de paire de clés publique/privée RSA ou du cryptage HMAC à l'aide d'un secret partagé. Cela garantit l'origine et la validité d'un jeton.
Tout cela se résume à un moyen sûr et efficace d'authentifier les utilisateurs, puis de vérifier les appels vers vos points de terminaison d'API sans avoir à analyser de structures de données ni à implémenter votre propre chiffrement.
Théorie des applications
Donc, avec un peu de contexte, nous pouvons maintenant nous plonger dans la façon dont cela fonctionnerait dans une application réelle. Pour cet exemple, je vais supposer que nous avons un serveur Node.js hébergeant notre API et que nous développons une liste de tâches SPA à l'aide d'Angular 6. Travaillons également avec cette structure d'API :
-
/auth
→POST
(poster le nom d'utilisateur et le mot de passe pour s'authentifier et recevoir en retour un JWT) -
/todos
→GET
(renvoie une liste d'éléments de la liste de tâches pour l'utilisateur) -
/todos/{id}
→GET
(renvoie un élément spécifique de la liste de tâches) -
/users
→GET
(renvoie une liste d'utilisateurs)
Nous passerons en revue la création de cette application simple sous peu, mais pour l'instant, concentrons-nous sur l'interaction en théorie. Nous avons une page de connexion simple, où l'utilisateur peut entrer son nom d'utilisateur et son mot de passe. Lorsque le formulaire est soumis, il envoie ces informations au point de terminaison /auth
. Le serveur Node peut ensuite authentifier l'utilisateur de la manière appropriée (recherche dans la base de données, interrogation d'un autre service Web, etc.), mais en fin de compte, le point de terminaison doit renvoyer un JWT.
Le JWT de cet exemple contiendra quelques revendications réservées et quelques revendications privées . Les revendications réservées sont simplement des paires clé-valeur recommandées par JWT couramment utilisées pour l'authentification, tandis que les revendications privées sont des paires clé-valeur applicables uniquement à notre application :
Réclamations réservées
-
iss
: Émetteur de ce jeton. Généralement le nom de domaine complet du serveur, mais peut être n'importe quoi tant que l'application client sait s'y attendre. -
exp
: date et heure d'expiration de ce jeton. Ceci est en secondes depuis le 1er janvier 1970 à minuit GMT (heure Unix). -
nbf
: non valide avant l'horodatage. Pas souvent utilisé, mais donne une limite inférieure pour la fenêtre de validité. Même format queexp
.
Revendications privées
-
uid
: ID utilisateur de l'utilisateur connecté. -
role
: Rôle attribué à l'utilisateur connecté.
Nos informations seront encodées en base64 et signées en utilisant HMAC avec la clé partagée todo-app-super-shared-secret
. Vous trouverez ci-dessous un exemple de ce à quoi ressemble le JWT :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ
Cette chaîne est tout ce dont nous avons besoin pour nous assurer que nous avons une connexion valide, pour savoir quel utilisateur est connecté et même pour savoir quel(s) rôle(s) l'utilisateur a.
La plupart des bibliothèques et des applications procèdent au stockage de ce JWT dans localStorage
ou sessionStorage
pour une récupération facile, mais ce n'est qu'une pratique courante. Ce que vous faites avec le jeton dépend de vous, tant que vous pouvez le fournir pour les futurs appels d'API.
Désormais, chaque fois que le SPA souhaite effectuer un appel vers l'un des points de terminaison d'API protégés, il doit simplement envoyer le jeton dans l'en-tête HTTP d' Authorization
.
Authorization: Bearer {JWT Token}
Remarque : Encore une fois, il s'agit simplement d'une pratique courante. JWT ne prescrit aucune méthode particulière pour s'envoyer au serveur. Vous pouvez également l'ajouter à l'URL ou l'envoyer dans un cookie.
Une fois que le serveur reçoit le JWT, il peut le décoder, assurer la cohérence à l'aide du secret partagé HMAC et vérifier l'expiration à l'aide des champs exp
et nbf
. Il peut également utiliser le champ iss
pour s'assurer qu'il s'agit de la partie émettrice d'origine de ce JWT.
Une fois que le serveur est satisfait de la validité du jeton, les informations stockées dans le JWT peuvent être utilisées. Par exemple, l' uid
que nous avons inclus nous donne l'ID de l'utilisateur qui fait la demande. Pour cet exemple particulier, nous avons également inclus le champ de role
, qui nous permet de décider si l'utilisateur doit pouvoir accéder à un point de terminaison particulier ou non. (Que vous fassiez confiance à ces informations ou que vous souhaitiez plutôt effectuer une recherche dans la base de données dépend du niveau de sécurité requis.)
function getTodos(jwtString) { var token = JWTDecode(jwtstring); if( Date.now() < token.nbf*1000) { throw new Error('Token not yet valid'); } if( Date.now() > token.exp*1000) { throw new Error('Token has expired'); } if( token.iss != 'todoapi') { throw new Error('Token not issued here'); } var userID = token.uid; var todos = loadUserTodosFromDB(userID); return JSON.stringify(todos); }
Construisons une application Todo simple
Pour suivre, vous devrez disposer d'une version récente de Node.js (6.x ou version ultérieure), npm (3.x ou version ultérieure) et angular-cli installé. Si vous devez installer Node.js, qui inclut npm, veuillez suivre les instructions ici. Ensuite angular-cli
peut être installé en utilisant npm
(ou yarn
, si vous l'avez installé):
# installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli
Je n'entrerai pas dans les détails du passe-partout Angular 6 que nous utiliserons ici, mais pour l'étape suivante, j'ai créé un référentiel Github pour contenir une petite application todo afin d'illustrer la simplicité d'ajout de l'authentification JWT à votre application. Clonez-le simplement en utilisant ce qui suit :
git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt
La git checkout pre-jwt
passe à une version nommée où JWT n'a pas été implémenté.
Il devrait y avoir deux dossiers à l'intérieur appelés server
et client
. Le serveur est un serveur Node API qui hébergera notre API de base. Le client est notre application Angular 6.
Le serveur d'API de nœud
Pour commencer, installez les dépendances et démarrez le serveur d'API.
cd server # installation using npm npm install # or installation using yarn yarn node app.js
Vous devriez pouvoir suivre ces liens et obtenir une représentation JSON des données. Pour l'instant, jusqu'à ce que nous ayons l'authentification, nous avons codé en dur le point de terminaison /todos
pour renvoyer les tâches pour userID=1
:
- http://localhost:4000 : page de test pour voir si le serveur Node est en cours d'exécution
- http://localhost:4000/api/users : renvoie la liste des utilisateurs du système
- http://localhost:4000/api/todos : renvoie la liste des tâches pour
userID=1
L'application angulaire
Pour démarrer avec l'application cliente, nous devons également installer les dépendances et démarrer le serveur de développement.
cd client # using npm npm install npm start # using yarn yarn yarn start
Remarque : Selon la vitesse de votre ligne, le téléchargement de toutes les dépendances peut prendre un certain temps.
Si tout se passe bien, vous devriez maintenant voir quelque chose comme ceci lorsque vous naviguez vers http://localhost:4200 :
Ajout d'une authentification via JWT
Pour ajouter la prise en charge de l'authentification JWT, nous utiliserons certaines bibliothèques standard disponibles qui le rendent plus simple. Vous pouvez, bien sûr, renoncer à ces commodités et tout mettre en œuvre vous-même, mais cela dépasse notre cadre ici.
Tout d'abord, installons une bibliothèque côté client. Il est développé et maintenu par Auth0, qui est une bibliothèque vous permettant d'ajouter une authentification basée sur le cloud à un site Web. L'utilisation de la bibliothèque elle-même ne nécessite pas que vous utilisiez leurs services.
cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt
Nous verrons le code dans une seconde, mais pendant que nous y sommes, configurons également le côté serveur. Nous utiliserons les bibliothèques body-parser
, jsonwebtoken
et express-jwt
pour que Node comprenne les corps JSON POST et les JWT.
cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt
Point de terminaison API pour l'authentification
Tout d'abord, nous avons besoin d'un moyen d'authentifier les utilisateurs avant de leur donner un jeton. Pour notre démonstration simple, nous allons simplement configurer un point de terminaison d'authentification fixe avec un nom d'utilisateur et un mot de passe codés en dur. Cela peut être aussi simple ou aussi complexe que votre application l'exige. L'important est de renvoyer un JWT.
Dans server/app.js
ajoutez une entrée sous les autres lignes require
comme suit :
const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');
Ainsi que les suivants :
app.use(bodyParser.json()); app.post('/api/auth', function(req, res) { const body = req.body; const user = USERS.find(user => user.username == body.username); if(!user || body.password != 'todo') return res.sendStatus(401); var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'}); res.send({token}); });
Il s'agit principalement de code JavaScript de base. Nous obtenons le corps JSON qui a été transmis au point de terminaison /auth
, trouvons un utilisateur correspondant à ce nom d'utilisateur, vérifions que nous avons un utilisateur et que le mot de passe correspondent, et renvoyons une erreur HTTP 401 Unauthorized
si ce n'est pas le cas.
La partie importante est la génération de jetons, et nous allons la décomposer en ses trois paramètres. La syntaxe de sign
est la suivante : jwt.sign(payload, secretOrPrivateKey, [options, callback])
, où :
-
payload
est un objet littéral de paires clé-valeur que vous souhaitez encoder dans votre jeton. Ces informations peuvent ensuite être décodées à partir du jeton par toute personne disposant de la clé de déchiffrement. Dans notre exemple, nous encodons leuser.id
afin que lorsque nous recevons à nouveau le jeton sur le back-end pour l'authentification, nous sachions à quel utilisateur nous avons affaire. -
secretOrPrivateKey
est soit une clé secrète partagée de chiffrement HMAC (c'est ce que nous avons utilisé dans notre application, pour plus de simplicité), soit une clé privée de chiffrement RSA/ECDSA. -
options
représente une variété d'options qui peuvent être transmises à l'encodeur sous la forme de paires clé-valeur. En règle générale, nous spécifions au moinsexpiresIn
(devient la revendication réservéeexp
) et l'issuer
(demande réservéeiss
) afin qu'un jeton ne soit pas valide pour toujours, et le serveur peut vérifier qu'il a bien émis le jeton à l'origine. -
callback
est une fonction à appeler une fois l'encodage terminé, si l'on souhaite gérer l'encodage du jeton de manière asynchrone.
(Vous pouvez également lire plus de détails sur les options
et comment utiliser la cryptographie à clé publique au lieu d'une clé secrète partagée.)
Intégration angulaire 6 JWT
Faire fonctionner Angular 6 avec notre JWT est assez simple en utilisant angular-jwt
. Ajoutez simplement ce qui suit à client/src/app/app.modules.ts
:
import { JwtModule } from '@auth0/angular-jwt'; // ... export function tokenGetter() { return localStorage.getItem('access_token'); } @NgModule({ // ... imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule, // Add this import here JwtModule.forRoot({ config: { tokenGetter: tokenGetter, whitelistedDomains: ['localhost:4000'], blacklistedRoutes: ['localhost:4000/api/auth'] } }) ], // ... }
C'est essentiellement tout ce qui est nécessaire. Bien sûr, nous avons encore du code à ajouter pour effectuer l'authentification initiale, mais la bibliothèque angular-jwt
se charge d'envoyer le jeton avec chaque requête HTTP.

- La fonction
tokenGetter()
fait exactement ce qu'elle dit, mais la façon dont elle est implémentée dépend entièrement de vous. Nous avons choisi de retourner le jeton que nous sauvegardons danslocalStorage
. Vous êtes bien sûr libre de fournir toute autre méthode que vous désirez, tant qu'elle renvoie la chaîne encodée du jeton Web JSON . - L'option
whiteListedDomains
existe afin que vous puissiez restreindre les domaines auxquels le JWT est envoyé, afin que les API publiques ne reçoivent pas également votre JWT. - L'option
blackListedRoutes
vous permet de spécifier des itinéraires spécifiques qui ne doivent pas recevoir le JWT même s'ils se trouvent sur un domaine en liste blanche. Par exemple, le point de terminaison d'authentification n'a pas besoin de le recevoir car cela ne sert à rien : le jeton est généralement nul lorsqu'il est appelé de toute façon.
Faire en sorte que tout fonctionne ensemble
À ce stade, nous avons un moyen de générer un JWT pour un utilisateur donné en utilisant le point de terminaison /auth
sur notre API, et nous avons la plomberie effectuée sur Angular pour envoyer un JWT avec chaque requête HTTP. Très bien, mais vous pourriez souligner qu'absolument rien n'a changé pour l'utilisateur. Et vous auriez raison. Nous pouvons toujours accéder à chaque page de notre application et nous pouvons appeler n'importe quel point de terminaison d'API sans même envoyer de JWT. Pas bon!
Nous devons mettre à jour notre application client pour nous préoccuper de savoir qui est connecté, et également mettre à jour notre API pour exiger un JWT. Commençons.
Nous aurons besoin d'un nouveau composant Angular pour nous connecter. Par souci de brièveté, je vais garder cela aussi simple que possible. Nous aurons également besoin d'un service qui gérera toutes nos exigences d'authentification et d'un garde angulaire pour protéger les routes qui ne devraient pas être accessibles avant la connexion. Nous ferons ce qui suit dans le contexte de l'application cliente.
cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false
Cela aurait dû générer quatre nouveaux fichiers dans le dossier client
:
src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts
Ensuite, nous devons fournir le service d'authentification et protéger notre application. Mettre à jour client/src/app/app.modules.ts
:
import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],
Et puis mettez à jour le routage dans client/src/app/app-routing.modules.ts
pour utiliser la protection d'authentification et fournir une route pour le composant de connexion.
// ... import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }, { path: 'users', component: UserListComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, // ...
Enfin, mettez à jour client/src/app/auth.guard.ts
avec le contenu suivant :
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (localStorage.getItem('access_token')) { return true; } this.router.navigate(['login']); return false; } }
Pour notre application de démonstration, nous vérifions simplement l'existence d'un JWT dans le stockage local. Dans les applications du monde réel, vous décodez le jeton et vérifiez sa validité, son expiration, etc. Par exemple, vous pouvez utiliser JwtHelperService pour cela.
À ce stade, notre application Angular vous redirigera désormais toujours vers la page de connexion puisque nous n'avons aucun moyen de nous connecter. Rectifions cela, en commençant par le service d'authentification dans client/src/app/auth.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable<boolean> { return this.http.post<{token: string}>('/api/auth', {username: username, password: password}) .pipe( map(result => { localStorage.setItem('access_token', result.token); return true; }) ); } logout() { localStorage.removeItem('access_token'); } public get loggedIn(): boolean { return (localStorage.getItem('access_token') !== null); } }
Notre service d'authentification n'a que deux fonctions, login
et logout
:
-
login
POST
envoie le nom d'username
et le mot depassword
fournis à notre serveur principal et définit leaccess_token
danslocalStorage
s'il en reçoit un en retour. Par souci de simplicité, il n'y a pas de gestion des erreurs ici. -
logout
efface simplementaccess_token
delocalStorage
, ce qui nécessite l'acquisition d'un nouveau jeton avant de pouvoir accéder à nouveau à quoi que ce soit d'autre. -
loggedIn
est une propriété booléenne que nous pouvons rapidement utiliser pour déterminer si l'utilisateur est connecté ou non.
Et enfin, le composant de connexion. Ceux-ci n'ont aucun rapport avec le travail réel avec JWT, alors n'hésitez pas à copier et coller dans client/src/app/login/login.components.html
:
<h4 *ngIf="error">{{error}}</h4> <form (ngSubmit)="submit()"> <div class="form-group col-3"> <label for="username">Username</label> <input type="text" name="username" class="form-control" [(ngModel)]="username" /> </div> <div class="form-group col-3"> <label for="password">Password</label> <input type="password" name="password" class="form-control" [(ngModel)]="password" /> </div> <div class="form-group col-3"> <button class="btn btn-primary" type="submit">Login</button> </div> </form>
Et client/src/app/login/login.components.ts
aura besoin de :
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public username: string; public password: string; public error: string; constructor(private auth: AuthService, private router: Router) { } public submit() { this.auth.login(this.username, this.password) .pipe(first()) .subscribe( result => this.router.navigate(['todos']), err => this.error = 'Could not authenticate' ); } }
Voila, notre exemple de connexion Angular 6 :
À ce stade, nous devrions pouvoir nous connecter (en utilisant jemma
, paul
ou sebastian
avec le mot de passe todo
) et revoir tous les écrans. Mais notre application affiche les mêmes en-têtes de navigation et aucun moyen de se déconnecter quel que soit l'état actuel. Réglons cela avant de passer à la correction de notre API.
Dans client/src/app/app.component.ts
, remplacez tout le fichier par ce qui suit :
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private auth: AuthService, private router: Router) { } logout() { this.auth.logout(); this.router.navigate(['login']); } }
Et pour client/src/app/app.component.html
remplacez la section <nav>
par ce qui suit :
<nav class="nav nav-pills"> <a class="nav-link" routerLink="todos" routerLinkActive="active" *ngIf="auth.loggedIn">Todo List</a> <a class="nav-link" routerLink="users" routerLinkActive="active" *ngIf="auth.loggedIn">Users</a> <a class="nav-link" routerLink="login" routerLinkActive="active" *ngIf="!auth.loggedIn">Login</a> <a class="nav-link" (click)="logout()" href="#" *ngIf="auth.loggedIn">Logout</a> </nav>
Nous avons rendu notre navigation sensible au contexte pour qu'elle n'affiche que certains éléments selon que l'utilisateur est connecté ou non. auth.loggedIn
peut, bien sûr, être utilisé partout où vous pouvez importer le service d'authentification.
Sécuriser l'API
Vous pensez peut-être que c'est génial… tout semble fonctionner à merveille . Mais essayez de vous connecter avec les trois noms d'utilisateur différents et vous remarquerez quelque chose : ils renvoient tous la même liste de tâches. Si nous jetons un coup d'œil à notre serveur API, nous pouvons voir que chaque utilisateur a en fait sa propre liste d'éléments, alors quoi de neuf ?
Eh bien, rappelez-vous que lorsque nous avons commencé, nous avons codé notre point de terminaison API /todos
pour toujours renvoyer la liste de tâches pour userID=1
. En effet, nous n'avions aucun moyen de savoir qui était l'utilisateur actuellement connecté.
Maintenant que nous le faisons, voyons à quel point il est facile de sécuriser nos points de terminaison et d'utiliser les informations encodées dans le JWT pour fournir l'identité d'utilisateur requise. Au départ, ajoutez cette ligne à votre fichier server/app.js
juste en dessous du dernier appel app.use()
:
app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));
Nous utilisons le middleware express-jwt
, lui disons quel est le secret partagé et spécifions un tableau de chemins pour lesquels il ne devrait pas nécessiter de JWT. Et c'est tout. Pas besoin de toucher à chaque point de terminaison, de créer des déclarations if
partout, ou quoi que ce soit.
En interne, le middleware fait quelques hypothèses. Par exemple, il suppose que l'en-tête HTTP d' Authorization
suit le modèle JWT commun de Bearer {token}
. (La bibliothèque a cependant de nombreuses options pour personnaliser son fonctionnement si ce n'est pas le cas. Voir Utilisation express-jwt pour plus de détails.)
Notre deuxième objectif est d'utiliser les informations codées JWT pour savoir qui passe l'appel. Une fois de plus, express-jwt
vient à la rescousse. Dans le cadre de la lecture et de la vérification du jeton, il définit la charge utile codée que nous avons envoyée dans le processus de signature à la variable req.user
dans Express. Nous pouvons ensuite l'utiliser pour accéder immédiatement à l'une des variables que nous avons stockées. Dans notre cas, nous définissons userID
égal à l'ID de l'utilisateur authentifié, et en tant que tel, nous pouvons l'utiliser directement comme req.user.userID
.
Mettez à nouveau à jour server/app.js
et modifiez le point de terminaison /todos
pour qu'il se lise comme suit :
res.send(getTodos(req.user.userID));
Et c'est tout. Notre API est désormais sécurisée contre tout accès non autorisé et nous pouvons déterminer en toute sécurité qui est notre utilisateur authentifié sur n'importe quel point de terminaison. Notre application cliente a également un processus d'authentification simple, et tous les services HTTP que nous écrivons et qui appellent notre point de terminaison API auront automatiquement un jeton d'authentification attaché.
Si vous avez cloné le référentiel Github et que vous souhaitez simplement voir le résultat final en action, vous pouvez consulter le code dans sa forme finale en utilisant :
git checkout with-jwt
J'espère que vous avez trouvé cette procédure pas à pas utile pour ajouter l'authentification JWT à vos propres applications Angular. Merci d'avoir lu!