Angular JWT User Authentication
Login
Créer le module
ng generate module authentification --routing
Créer le composant
ng g c authentification/login
Créer la classe
ng generate class authentification/user
export class User {
_id:object;
email:string;
password:string;
firstname:string;
lastname:string;
}
Créer le service
ng generate service authentification/auth
import { Injectable } from '@angular/core';
import { User } from './user';
import { Observable } from 'rxjs';
import {HttpClient} from '@angular/common/http';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private baseurl="http://localhost:3001/api/users"
constructor(private http: HttpClient, public router: Router) { }
// Sign-up
signUp(user: User): Observable<any> {
return this.http.post(this.baseurl + '/register/', user);
}
// Sign-in
signIn(user: any) {
return this.http
.post<any>(this.baseurl + "/login/" , user)
.subscribe({
next: (res:any) => {
localStorage.setItem('access_token', res.token);
},
error: (e:any) => {
console.log(e);
alert("Error !")
},
complete: () => {
this.router.navigate(['products']);
}
});
}
getToken() {
return localStorage.getItem('access_token');
}
getisLoggedIn(): boolean {
let authToken = localStorage.getItem('access_token');
return authToken !== null ? true : false;
}
doLogout() {
let removeToken = localStorage.removeItem('access_token');
if (removeToken == null) {
this.router.navigate(['login']);
}
}
Créer le composant src>app>authentification/register
ng g c authentification/register --skip-tests
ouvrir le fichier register.component.ts
import { Component } from '@angular/core';
import { User } from '../user';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent {
constructor(private authserv:AuthService,private router:Router){}
user:User=new User()
Ajoutuser=()=>{
this.authserv.signUp(this.user).subscribe((data=>{
this.router.navigate(['login']);
}))}
Fichier register.component.html
<form (ngSubmit)="Ajoutuser()" >
<div class="mb-3">
<input type="text" class="form-control" id="email" name = "email"
placeholder="email" [(ngModel)]="user.email" >
</div>
<div class="mb-3">
<input type="text" class="form-control" id="password" name =
"password" placeholder="password" [(ngModel)]="user.password" >
</div>
<div class="mb-3">
<input type="text" class="form-control" id="firstname" name =
"firstname" placeholder="firstname" [(ngModel)]="user.firstname" >
</div>
<div class="mb-3">
<input type="text" class="form-control" id="lastname" name =
"lastname" placeholder="lastname" [(ngModel)]="user.lastname" >
</div>
<div>
<button type="submit" class="btn btn-success">Enregistrer</button>
</div>
</form>
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthentificationRoutingModule } from './authentification-
routing.module';
import { LoginComponent } from './login/login.component';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
LoginComponent
],
imports: [
CommonModule,
AuthentificationRoutingModule,
FormsModule,
]
})
export class AuthentificationModule { }
Fichier Authentification-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RegisterComponent } from './register/register.component';
const routes: Routes = [
{ path: 'register', component: RegisterComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthentificationRoutingModule { }
Créer le composant src>app>authentification/login
ng g c authentification/login --skip-tests
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
import { User } from '../user';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
constructor(public authService: AuthService,public router: Router){}
email:string=''
password:string=''
loginUser() {
const userlogin={
email:this.email,
password:this.password
}
this.authService.signIn(userlogin);
}
<div class="form-signin w-50 m-auto">
<form (ngSubmit)="loginUser()">
<h3 class="h3 mb-3 font-weight-normal text-center">Login</h3>
<div class="mb-3">
<label>Email</label>
<div class="mb-3">
<input type="text" class="form-control" id="email" name = "email"
placeholder="email" [(ngModel)]="email" >
</div>
</div>
<div class="mb-3">
<label>Password</label>
<div class="mb-3">
<input type="text" class="form-control" id="password" name =
"password" placeholder="password" [(ngModel)]="password" >
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Sign in</button>
</div>
</form>
</div>
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthentificationRoutingModule { }
import { AuthentificationModule } from
'./authentification/authentification.module';
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
ProductsModule,
AuthentificationModule
],
Logout
src >app>logout
ng g c authentification/logout --skip-tests
fichier Logout.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-logout',
templateUrl: './logout.component.html',
styleUrls: ['./logout.component.css']
})
export class LogoutComponent {
constructor(public authService: AuthService) { }
ngOnInit(){
this.logout()
}
logout() {
this.authService.doLogout()
}
<div class="container d-flex flex-wrap p-3 px-md-4 mb-3 bg-white border-
bottom">
<nav class="navbar navbar-light bg-light">
<a *ngIf="this.authService.isLoggedIn" class="p-2 text-dark"
routerLink="/products">Liste des Produits</a>
<a
*ngIf="!this.authService.isLoggedIn"
class="btn btn-light text-dark me-2"
routerLinkActive="active"
routerLink="/log-in">Sign in</a
>
</nav>
<button
(click)="logout()"
*ngIf="this.authService.isLoggedIn"
type="button"
class="btn btn-default">
Logout
</button>
</div>
<div class="container mt-5">
<div class="mt-5">
<router-outlet></router-outlet>
</div>
</div>
Protected Routes avec CanActivate
ng g guard authentification/auth
import { CanActivateFn } from '@angular/router';
import {
Router
} from '@angular/router';
import { AuthService } from './auth.service';
import { inject } from '@angular/core';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.getisLoggedIn()!==true) {
window.alert('Access not allowed!');
router.navigate(['login']);
}
return true;
};
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IndexComponent } from './index/index.component';
import { AuthGuard } from '../authentification/auth.guard';
const routes: Routes = [
{ path: 'products', component: IndexComponent , canActivate: [AuthGuard] },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }
Refresh Token
import { Injectable } from '@angular/core';
import { User } from './user';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import {
HttpClient,
HttpHeaders,
HttpErrorResponse,
} from '@angular/common/http';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthService {
endpoint: string = 'https://backend-commerce-2023-jwt.vercel.app/api';
headers = new HttpHeaders().set('Content-Type', 'application/json');
currentUser = {};
constructor(private http: HttpClient, public router: Router) {}
// Sign-up
signUp(user: User): Observable<any> {
let api = `${this.endpoint}/users/register/`;
return this.http.post(api, user).pipe(catchError(this.handleError));
}
// Sign-in
signIn(user: User) {
return this.http
.post<any>(`${this.endpoint}/users/login`, user)
.subscribe({
next: (res:any) => {
localStorage.setItem('access_token', res.token);
localStorage.setItem('refresh_token', res.refreshToken);
},
error: (e:any) => {
console.log(e);
alert("Error !")
},
complete: () => {
this.router.navigate(['products']);
}
});
}
getToken() {
return localStorage.getItem('access_token');
}
get isLoggedIn(): boolean {
let authToken = localStorage.getItem('access_token');
return authToken !== null ? true : false;
}
doLogout() {
let removeToken = localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
if (removeToken == null) {
this.router.navigate(['log-in']);
}
}
// Error
handleError(error: HttpErrorResponse) {
let msg = '';
if (error.error instanceof ErrorEvent) {
// client-side error
msg = error.error.message;
} else {
// server-side error
msg = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
return throwError(msg);
}
//refresh
refreshToken(token: string) {
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
return this.http.post(`${this.endpoint}/users/refreshToken/`, {
refreshToken: token
}, httpOptions);
}
import { HTTP_INTERCEPTORS, HttpEvent, HttpErrorResponse } from
'@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest } from
'@angular/common/http';
import { AuthService } from "./auth.service";
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new
BehaviorSubject<any>(null);
constructor(private authService: AuthService) { }
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<Object>> {
let authReq = req;
const authToken = this.authService.getToken();
if (authToken != null) {
authReq = this.addTokenHeader(req, authToken);
}
return next.handle(authReq).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 403) {
return this.handle403Error(authReq, next);
}
return throwError(error);
}));
}
private handle403Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
const token = localStorage.getItem('refresh_token');
/*
nous devons appeler la fonction refreshToken()
qui utilise HttpClient pour envoyer une requête HTTP
avec rafraîchirToken dans le body.
*/
/*
L'Angular SwitchMap mappe chaque valeur de l'observable source vers un
observable interne,
s'y abonne, puis commence à en émettre les valeurs.
Il crée un nouvel observable intérieur pour chaque valeur qu'il reçoit de la
Source.
Chaque fois qu'il crée un nouvel observable interne,
il se désabonne de tous les observables internes précédemment créés.
Fondamentalement, il passe au plus récent observable en éliminant tous les
autres.
*/
if (token)
return this.authService.refreshToken(token).pipe(
switchMap((res: any) => {
this.isRefreshing = false;
localStorage.setItem('access_token', res.token);
localStorage.setItem('refresh_token', res.refreshToken);
this.refreshTokenSubject.next(res.token);
return next.handle(this.addTokenHeader(request, res.token));
}),
catchError((err) => {
this.isRefreshing = false;
this.authService.doLogout();
return throwError(err);
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap((token) => next.handle(this.addTokenHeader(request,
token)))
);
}
private addTokenHeader(request: HttpRequest<any>, token: string) {
return request.clone({ setHeaders: {
Authorization: "Bearer " + token
}
});
}
}
export const authInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];
Après une minute (délai d’expiration)
On voit l’erreur de code 403 qui invoque la requête de RefreshToken et génère le nouveau token. Ce
qui permet d’afficher la liste des produits avec l’autorisation du nouveau token.